diff --git a/dependencies.gradle b/dependencies.gradle
index 87b8e3c12ffabfac2fa80507b8587ef76d1cb181..7666a3bf9f8b4cc9929a65f2b0822eaae753cb33 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -9,13 +9,13 @@ ext.versions = [
 
 def gradle = "7.0.4"
 // Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.5.31"
-def kotlinCoroutines = "1.5.2"
+def kotlin = "1.6.0"
+def kotlinCoroutines = "1.6.0"
 def dagger = "2.40.5"
 def retrofit = "2.9.0"
 def arrow = "0.8.2"
 def markwon = "4.6.2"
-def moshi = "1.12.0"
+def moshi = "1.13.0"
 def lifecycle = "2.4.0"
 def flowBinding = "1.2.0"
 def epoxy = "4.6.2"
@@ -58,6 +58,7 @@ ext.libs = [
                 'lifecycleCommon'         : "androidx.lifecycle:lifecycle-common:$lifecycle",
                 'lifecycleLivedata'       : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
                 'lifecycleProcess'        : "androidx.lifecycle:lifecycle-process:$lifecycle",
+                'lifecycleRuntimeKtx'     : "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle",
                 'datastore'               : "androidx.datastore:datastore:1.0.0",
                 'datastorepreferences'    : "androidx.datastore:datastore-preferences:1.0.0",
                 'pagingRuntimeKtx'        : "androidx.paging:paging-runtime-ktx:2.1.2",
@@ -141,4 +142,4 @@ ext.libs = [
                 'timberJunitRule'        : "net.lachlanmckee:timber-junit-rule:1.0.1",
                 'junit'                  : "junit:junit:4.13.2"
         ]
-]
\ No newline at end of file
+]
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index 45883f506d4326389bd26e24fb69856ea533e605..9e70a1de421068dc5f1e8989cea91d7f4a69558c 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -7,6 +7,7 @@ ext.groups = [
                         'com.github.chrisbanes',
                         'com.github.hyuwah',
                         'com.github.jetradarmobile',
+                        'com.github.MatrixFrog',
                         'com.github.tapadoo',
                         'com.github.vector-im',
                         'com.github.yalantis',
@@ -39,6 +40,7 @@ ext.groups = [
                 regex: [
                 ],
                 group: [
+                        'ch.qos.logback',
                         'com.adevinta.android',
                         'com.airbnb.android',
                         'com.almworks.sqlite4java',
@@ -48,10 +50,12 @@ ext.groups = [
                         'com.beust',
                         'com.davemorrissey.labs',
                         'com.dropbox.core',
+                        'com.facebook.fbjni',
                         'com.facebook.fresco',
                         'com.facebook.infer.annotation',
                         'com.facebook.soloader',
                         'com.facebook.stetho',
+                        'com.facebook.yoga',
                         'com.fasterxml',
                         'com.fasterxml.jackson',
                         'com.fasterxml.jackson.core',
@@ -113,6 +117,7 @@ ext.groups = [
                         'info.picocli',
                         'io.arrow-kt',
                         'io.github.detekt.sarif4k',
+                        'io.github.microutils',
                         'io.github.reactivecircus.flowbinding',
                         'io.grpc',
                         'io.jsonwebtoken',
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 6605acd29384c4104fd1e77cddbff46d6aafa9c6..ec4de9615ab3aa11d8105a0fd06eb5e2600182d5 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -37,9 +37,8 @@ android {
         buildConfigField "String", "SDK_VERSION", "\"${project.getProperties().getOrDefault("VERSION_NAME", "0.0.0")}\""
 
         buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
-        resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
-        resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
-        resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
+        buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
+        buildConfigField "String", "GIT_SDK_REVISION_DATE", "\"${gitRevisionDate()}\""
 
         defaultConfig {
             consumerProguardFiles 'proguard-rules.pro'
@@ -170,7 +169,7 @@ dependencies {
     implementation libs.apache.commonsImaging
 
     // Phone number https://github.com/google/libphonenumber
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.44'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.46'
 
     testImplementation libs.tests.junit
     testImplementation 'org.robolectric:robolectric:4.7.3'
@@ -178,7 +177,7 @@ dependencies {
     // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
     testImplementation libs.mockk.mockk
     testImplementation libs.tests.kluent
-    implementation libs.jetbrains.coroutinesAndroid
+    testImplementation libs.jetbrains.coroutinesTest
     // Plant Timber tree for test
     testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
     // Transitively required for mocking realm as monarchy doesn't expose Rx
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt
index 5c9b79361e891d545fbaaea1774a68f8c6f75145..0f79896b2cd28a6f42bdc7fd19ded9a3a15c1dbf 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt
@@ -23,7 +23,7 @@ object TestConstants {
     const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080"
 
     // Time out to use when waiting for server response.
-    private const val AWAIT_TIME_OUT_MILLIS = 30_000
+    private const val AWAIT_TIME_OUT_MILLIS = 60_000
 
     // Time out to use when waiting for server response, when the debugger is connected. 10 minutes
     private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
new file mode 100644
index 0000000000000000000000000000000000000000..41ec69cdc5cae520e6ce371ad63a3b932a424dac
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
@@ -0,0 +1,649 @@
+/*
+ * 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.crypto
+
+import android.util.Log
+import androidx.test.filters.LargeTest
+import kotlinx.coroutines.delay
+import org.amshove.kluent.fail
+import org.amshove.kluent.internal.assertEquals
+import org.junit.Assert
+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.Session
+import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.Room
+import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.send.SendState
+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 org.matrix.android.sdk.common.SessionTestParams
+import org.matrix.android.sdk.common.TestConstants
+import org.matrix.android.sdk.common.TestMatrixCallback
+import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
+import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
+import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
+import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
+import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
+import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+@LargeTest
+class E2eeSanityTests : InstrumentedTest {
+
+    private val testHelper = CommonTestHelper(context())
+    private val cryptoTestHelper = CryptoTestHelper(testHelper)
+
+    /**
+     * Simple test that create an e2ee room.
+     * Some new members are added, and a message is sent.
+     * We check that the message is e2e and can be decrypted.
+     *
+     * Additional users join, we check that they can't decrypt history
+     *
+     * Alice sends a new message, then check that the new one can be decrypted
+     */
+    @Test
+    fun testSendingE2EEMessages() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+        val aliceSession = cryptoTestData.firstSession
+        val e2eRoomID = cryptoTestData.roomId
+
+        val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+        // add some more users and invite them
+        val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
+                .map {
+                    testHelper.createAccount(it, SessionTestParams(true))
+                }
+
+        Log.v("#E2E TEST", "All accounts created")
+        // we want to invite them in the room
+        otherAccounts.forEach {
+            testHelper.runBlockingTest {
+                Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
+                aliceRoomPOV.invite(it.myUserId)
+            }
+        }
+
+        // All user should accept invite
+        otherAccounts.forEach { otherSession ->
+            waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
+            Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
+        }
+
+        // check that alice see them as joined (not really necessary?)
+        ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID)
+
+        Log.v("#E2E TEST", "All users have joined the room")
+        Log.v("#E2E TEST", "Alice is sending the message")
+
+        val text = "This is my message"
+        val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
+        //        val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
+        Assert.assertTrue("Message should be sent", sentEventId != null)
+
+        // All should be able to decrypt
+        otherAccounts.forEach { otherSession ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+        }
+
+        // Add a new user to the room, and check that he can't decrypt
+        val newAccount = listOf("adam") // , "adam", "manu")
+                .map {
+                    testHelper.createAccount(it, SessionTestParams(true))
+                }
+
+        newAccount.forEach {
+            testHelper.runBlockingTest {
+                Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
+                aliceRoomPOV.invite(it.myUserId)
+            }
+        }
+
+        newAccount.forEach {
+            waitForAndAcceptInviteInRoom(it, e2eRoomID)
+        }
+
+        ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID)
+
+        // wait a bit
+        testHelper.runBlockingTest {
+            delay(3_000)
+        }
+
+        // check that messages are encrypted (uisi)
+        newAccount.forEach { otherSession ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also {
+                        Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
+                    }
+                    timelineEvent != null &&
+                            timelineEvent.root.getClearType() == EventType.ENCRYPTED &&
+                            timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
+                }
+            }
+        }
+
+        // Let alice send a new message
+        Log.v("#E2E TEST", "Alice sends a new message")
+
+        val secondMessage = "2 This is my message"
+        val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
+
+        // new members should be able to decrypt it
+        newAccount.forEach { otherSession ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also {
+                        Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
+                    }
+                    timelineEvent != null &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE &&
+                            secondMessage == timelineEvent.root.getClearContent().toModel<MessageContent>()?.body
+                }
+            }
+        }
+
+        otherAccounts.forEach {
+            testHelper.signOutAndClose(it)
+        }
+        newAccount.forEach { testHelper.signOutAndClose(it) }
+
+        cryptoTestData.cleanUp(testHelper)
+    }
+
+    /**
+     * Quick test for basic key backup
+     * 1. Create e2e between Alice and Bob
+     * 2. Alice sends 3 messages, using 3 different sessions
+     * 3. Ensure bob can decrypt
+     * 4. Create backup for bob and upload keys
+     *
+     * 5. Sign out alice and bob to ensure no gossiping will happen
+     *
+     * 6. Let bob sign in with a new session
+     * 7. Ensure history is UISI
+     * 8. Import backup
+     * 9. Check that new session can decrypt
+     */
+    @Test
+    fun testBasicBackupImport() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+        val aliceSession = cryptoTestData.firstSession
+        val bobSession = cryptoTestData.secondSession!!
+        val e2eRoomID = cryptoTestData.roomId
+
+        Log.v("#E2E TEST", "Create and start key backup for bob ...")
+        val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
+        val keyBackupPassword = "FooBarBaz"
+        val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> {
+            bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
+        }
+        val version = testHelper.doSync<KeysVersion> {
+            bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
+        }
+        Log.v("#E2E TEST", "... Key backup started and enabled for bob")
+        // Bob session should now have
+
+        val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+        // let's send a few message to bob
+        val sentEventIds = mutableListOf<String>()
+        val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
+        messagesText.forEach { text ->
+            val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
+                sentEventIds.add(it)
+            }
+
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+            // we want more so let's discard the session
+            aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
+
+            testHelper.runBlockingTest {
+                delay(1_000)
+            }
+        }
+        Log.v("#E2E TEST", "Bob received all and can decrypt")
+
+        // Let's wait a bit to be sure that bob has backed up the session
+
+        Log.v("#E2E TEST", "Force key backup for Bob...")
+        testHelper.waitWithLatch { latch ->
+            bobKeysBackupService.backupAllGroupSessions(
+                    null,
+                    TestMatrixCallback(latch, true)
+            )
+        }
+        Log.v("#E2E TEST", "... Key backup done for Bob")
+
+        // Now lets logout both alice and bob to ensure that we won't have any gossiping
+
+        val bobUserId = bobSession.myUserId
+        Log.v("#E2E TEST", "Logout alice and bob...")
+        testHelper.signOutAndClose(aliceSession)
+        testHelper.signOutAndClose(bobSession)
+        Log.v("#E2E TEST", "..Logout alice and bob...")
+
+        testHelper.runBlockingTest {
+            delay(1_000)
+        }
+
+        // Create a new session for bob
+        Log.v("#E2E TEST", "Create a new session for Bob")
+        val newBobSession = testHelper.logIntoAccount(bobUserId, SessionTestParams(true))
+
+        // check that bob can't currently decrypt
+        Log.v("#E2E TEST", "check that bob can't currently decrypt")
+        sentEventIds.forEach { sentEventId ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also {
+                        Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
+                    }
+                    timelineEvent != null &&
+                            timelineEvent.root.getClearType() == EventType.ENCRYPTED
+                }
+            }
+        }
+        // after initial sync events are not decrypted, so we have to try manually
+        ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+
+        // Let's now import keys from backup
+
+        newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
+            val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
+                keysBackupService.getVersion(version.version, it)
+            }
+
+            val importedResult = testHelper.doSync<ImportRoomKeysResult> {
+                keysBackupService.restoreKeyBackupWithPassword(keyVersionResult!!,
+                        keyBackupPassword,
+                        null,
+                        null,
+                        null, it)
+            }
+
+            assertEquals(3, importedResult.totalNumberOfKeys)
+        }
+
+        // ensure bob can now decrypt
+        ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
+
+        testHelper.signOutAndClose(newBobSession)
+    }
+
+    /**
+     * Check that a new verified session that was not supposed to get the keys initially will
+     * get them from an older one.
+     */
+    @Test
+    fun testSimpleGossip() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+        val aliceSession = cryptoTestData.firstSession
+        val bobSession = cryptoTestData.secondSession!!
+        val e2eRoomID = cryptoTestData.roomId
+
+        val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+        cryptoTestHelper.initializeCrossSigning(bobSession)
+
+        // let's send a few message to bob
+        val sentEventIds = mutableListOf<String>()
+        val messagesText = listOf("1. Hello", "2. Bob")
+
+        Log.v("#E2E TEST", "Alice sends some messages")
+        messagesText.forEach { text ->
+            val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
+                sentEventIds.add(it)
+            }
+
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+        }
+
+        // Ensure bob can decrypt
+        ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID)
+
+        // Let's now add a new bob session
+        // Create a new session for bob
+        Log.v("#E2E TEST", "Create a new session for Bob")
+        val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
+
+        // check that new bob can't currently decrypt
+        Log.v("#E2E TEST", "check that new bob can't currently decrypt")
+
+        ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+
+        // Try to request
+        sentEventIds.forEach { sentEventId ->
+            val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+            newBobSession.cryptoService().requestRoomKeyForEvent(event)
+        }
+
+        // wait a bit
+        testHelper.runBlockingTest {
+            delay(10_000)
+        }
+
+        // Ensure that new bob still can't decrypt (keys must have been withheld)
+        ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD)
+
+        // Now mark new bob session as verified
+
+        bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
+        newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!)
+
+        // now let new session re-request
+        sentEventIds.forEach { sentEventId ->
+            val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+            newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
+        }
+
+        // wait a bit
+        testHelper.runBlockingTest {
+            delay(10_000)
+        }
+
+        ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
+
+        cryptoTestData.cleanUp(testHelper)
+        testHelper.signOutAndClose(newBobSession)
+    }
+
+    /**
+     * Test that if a better key is forwarded (lower index, it is then used)
+     */
+    @Test
+    fun testForwardBetterKey() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+        val aliceSession = cryptoTestData.firstSession
+        val bobSessionWithBetterKey = cryptoTestData.secondSession!!
+        val e2eRoomID = cryptoTestData.roomId
+
+        val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+        cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey)
+
+        // let's send a few message to bob
+        var firstEventId: String
+        val firstMessage = "1. Hello"
+
+        Log.v("#E2E TEST", "Alice sends some messages")
+        firstMessage.let { text ->
+            firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
+
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+        }
+
+        // Ensure bob can decrypt
+        ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
+
+        // Let's add a new unverified session from bob
+        val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
+
+        // check that new bob can't currently decrypt
+        Log.v("#E2E TEST", "check that new bob can't currently decrypt")
+        ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+
+        // Now let alice send a new message. this time the new bob session will be able to decrypt
+        var secondEventId: String
+        val secondMessage = "2. New Device?"
+
+        Log.v("#E2E TEST", "Alice sends some messages")
+        secondMessage.let { text ->
+            secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
+
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+        }
+
+        // check that both messages have same sessionId (it's just that we don't have index 0)
+        val firstEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
+        val secondEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
+
+        val firstSessionId = firstEventNewBobPov!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
+        val secondSessionId = secondEventNewBobPov!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
+
+        Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId)
+
+        // Confirm we can decrypt one but not the other
+        testHelper.runBlockingTest {
+            try {
+                newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
+                fail("Should not be able to decrypt event")
+            } catch (error: MXCryptoError) {
+                val errorType = (error as? MXCryptoError.Base)?.errorType
+                assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType)
+            }
+        }
+
+        testHelper.runBlockingTest {
+            try {
+                newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
+            } catch (error: MXCryptoError) {
+                fail("Should be able to decrypt event")
+            }
+        }
+
+        // Now let's verify bobs session, and re-request keys
+        bobSessionWithBetterKey.cryptoService()
+                .verificationService()
+                .markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
+
+        newBobSession.cryptoService()
+                .verificationService()
+                .markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
+
+        // now let new session request
+        newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root)
+
+        // wait a bit
+        testHelper.runBlockingTest {
+            delay(10_000)
+        }
+
+        // old session should have shared the key at earliest known index now
+        // we should be able to decrypt both
+        testHelper.runBlockingTest {
+            try {
+                newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
+            } catch (error: MXCryptoError) {
+                fail("Should be able to decrypt first event now $error")
+            }
+        }
+        testHelper.runBlockingTest {
+            try {
+                newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
+            } catch (error: MXCryptoError) {
+                fail("Should be able to decrypt event $error")
+            }
+        }
+
+        cryptoTestData.cleanUp(testHelper)
+        testHelper.signOutAndClose(newBobSession)
+    }
+
+    private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
+        aliceRoomPOV.sendTextMessage(text)
+        var sentEventId: String? = null
+        testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
+            val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
+            timeline.start()
+
+            testHelper.retryPeriodicallyWithLatch(latch) {
+                val decryptedMsg = timeline.getSnapshot()
+                        .filter { it.root.getClearType() == EventType.MESSAGE }
+                        .also { list ->
+                            val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
+                            Log.v("#E2E TEST", "Timeline snapshot is $message")
+                        }
+                        .filter { it.root.sendState == SendState.SYNCED }
+                        .firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
+                sentEventId = decryptedMsg?.eventId
+                decryptedMsg != null
+            }
+
+            timeline.dispose()
+        }
+        return sentEventId
+    }
+
+    private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
+        testHelper.waitWithLatch { latch ->
+            testHelper.retryPeriodicallyWithLatch(latch) {
+                otherAccounts.map {
+                    aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership
+                }.all {
+                    it == Membership.JOIN
+                }
+            }
+        }
+    }
+
+    private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
+        testHelper.waitWithLatch { latch ->
+            testHelper.retryPeriodicallyWithLatch(latch) {
+                val roomSummary = otherSession.getRoomSummary(e2eRoomID)
+                (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
+                    if (it) {
+                        Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
+                    }
+                }
+            }
+        }
+
+        testHelper.runBlockingTest(60_000) {
+            Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
+            try {
+                otherSession.joinRoom(e2eRoomID)
+            } catch (ex: JoinRoomFailure.JoinedWithTimeout) {
+                // it's ok we will wait after
+            }
+        }
+
+        Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
+        testHelper.waitWithLatch {
+            testHelper.retryPeriodicallyWithLatch(it) {
+                val roomSummary = otherSession.getRoomSummary(e2eRoomID)
+                roomSummary != null && roomSummary.membership == Membership.JOIN
+            }
+        }
+    }
+
+    private fun ensureCanDecrypt(sentEventIds: MutableList<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
+        sentEventIds.forEachIndexed { index, sentEventId ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val event = session.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+                    testHelper.runBlockingTest {
+                        try {
+                            session.cryptoService().decryptEvent(event, "").let { result ->
+                                event.mxDecryptionResult = OlmDecryptionResult(
+                                        payload = result.clearEvent,
+                                        senderKey = result.senderCurve25519Key,
+                                        keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
+                                        forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
+                                )
+                            }
+                        } catch (error: MXCryptoError) {
+                            // nop
+                        }
+                    }
+                    event.getClearType() == EventType.MESSAGE &&
+                            messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
+                }
+            }
+        }
+    }
+
+    private fun ensureIsDecrypted(sentEventIds: List<String>, session: Session, e2eRoomID: String) {
+        testHelper.waitWithLatch { latch ->
+            sentEventIds.forEach { sentEventId ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+        }
+    }
+
+    private fun ensureCannotDecrypt(sentEventIds: List<String>, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) {
+        sentEventIds.forEach { sentEventId ->
+            val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+            testHelper.runBlockingTest {
+                try {
+                    newBobSession.cryptoService().decryptEvent(event, "")
+                    fail("Should not be able to decrypt event")
+                } catch (error: MXCryptoError) {
+                    val errorType = (error as? MXCryptoError.Base)?.errorType
+                    if (expectedError == null) {
+                        Assert.assertNotNull(errorType)
+                    } else {
+                        assertEquals(expectedError, errorType, "Message expected to be UISI")
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
index a7a81bacf50bd5d4775288d3b1e42130ba944625..46c1dacf782ec1b73a4a428d12fd64410f24f36e 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
@@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
 import org.junit.FixMethodOrder
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -41,7 +40,6 @@ class PreShareKeysTest : InstrumentedTest {
     private val cryptoTestHelper = CryptoTestHelper(testHelper)
 
     @Test
-    @Ignore("This test will be ignored until it is fixed")
     fun ensure_outbound_session_happy_path() {
         val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
         val e2eRoomID = testData.roomId
@@ -92,7 +90,7 @@ class PreShareKeysTest : InstrumentedTest {
         // Just send a real message as test
         val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first()
 
-        assertEquals(megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId, "Unexpected megolm session")
+        assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId,)
         testHelper.waitWithLatch { latch ->
             testHelper.retryPeriodicallyWithLatch(latch) {
                 bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
index 0a8ce6768078f5d9160ec060853821990abbacf3..fb5d58b127f5440dc8ca40f189ca19a9a8ef1bad 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
@@ -21,7 +21,6 @@ import org.amshove.kluent.shouldBe
 import org.junit.Assert
 import org.junit.Before
 import org.junit.FixMethodOrder
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -85,7 +84,6 @@ class UnwedgingTest : InstrumentedTest {
      * -> This is automatically fixed after SDKs restarted the olm session
      */
     @Test
-    @Ignore("This test will be ignored until it is fixed")
     fun testUnwedging() {
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
 
@@ -94,9 +92,7 @@ class UnwedgingTest : InstrumentedTest {
         val bobSession = cryptoTestData.secondSession!!
 
         val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
-
-        // bobSession.cryptoService().setWarnOnUnknownDevices(false)
-        // aliceSession.cryptoService().setWarnOnUnknownDevices(false)
+        val olmDevice = (aliceSession.cryptoService() as DefaultCryptoService).olmDeviceForTest
 
         val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
         val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
@@ -175,6 +171,7 @@ class UnwedgingTest : InstrumentedTest {
         Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message")
 
         aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm<OlmSession>(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!)
+        olmDevice.clearOlmSessionCache()
         Thread.sleep(6_000)
 
         // Force new session, and key share
@@ -227,8 +224,10 @@ class UnwedgingTest : InstrumentedTest {
         testHelper.waitWithLatch {
             testHelper.retryPeriodicallyWithLatch(it) {
                 // we should get back the key and be able to decrypt
-                val result = tryOrNull {
-                    bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
+                val result = testHelper.runBlockingTest {
+                    tryOrNull {
+                        bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
+                    }
                 }
                 Timber.i("## CRYPTO | testUnwedging: decrypt result  ${result?.clearEvent}")
                 result != null
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
index 82aee454eb361657f26b6f68748290b7272cfca4..cd20ab477ca535b0e30a4fa3d9ec8fb0d95681a9 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
@@ -97,7 +97,9 @@ class KeyShareTests : InstrumentedTest {
         assert(receivedEvent!!.isEncrypted())
 
         try {
-            aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            commonTestHelper.runBlockingTest {
+                aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            }
             fail("should fail")
         } catch (failure: Throwable) {
         }
@@ -152,7 +154,9 @@ class KeyShareTests : InstrumentedTest {
         }
 
         try {
-            aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            commonTestHelper.runBlockingTest {
+                aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            }
             fail("should fail")
         } catch (failure: Throwable) {
         }
@@ -189,7 +193,9 @@ class KeyShareTests : InstrumentedTest {
         }
 
         try {
-            aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            commonTestHelper.runBlockingTest {
+                aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            }
         } catch (failure: Throwable) {
             fail("should have been able to decrypt")
         }
@@ -384,7 +390,11 @@ class KeyShareTests : InstrumentedTest {
         val roomRoomBobPov = aliceSession.getRoom(roomId)
         val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
 
-        var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
+        var dRes = tryOrNull {
+            commonTestHelper.runBlockingTest {
+                bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "")
+            }
+        }
 
         assert(dRes == null)
 
@@ -395,7 +405,11 @@ class KeyShareTests : InstrumentedTest {
         Thread.sleep(3_000)
 
         // With the bug the first session would have improperly reshare that key :/
-        dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") }
+        dRes = tryOrNull {
+            commonTestHelper.runBlockingTest {
+                bobSession.cryptoService().decryptEvent(beforeJoin.root, "")
+            }
+        }
         Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}")
         assert(dRes?.clearEvent == null)
     }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
index 9fda21763a023c94f0d01f55cad1956ef9599d4a..e8f6eea460e33441ceccbbcec20db4aeb279f26f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
@@ -93,7 +93,9 @@ class WithHeldTests : InstrumentedTest {
         // Bob should not be able to decrypt because the keys is withheld
         try {
             // .. might need to wait a bit for stability?
-            bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+            testHelper.runBlockingTest {
+                bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+            }
             Assert.fail("This session should not be able to decrypt")
         } catch (failure: Throwable) {
             val type = (failure as MXCryptoError.Base).errorType
@@ -118,7 +120,9 @@ class WithHeldTests : InstrumentedTest {
         // Previous message should still be undecryptable (partially withheld session)
         try {
             // .. might need to wait a bit for stability?
-            bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+            testHelper.runBlockingTest {
+                bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+            }
             Assert.fail("This session should not be able to decrypt")
         } catch (failure: Throwable) {
             val type = (failure as MXCryptoError.Base).errorType
@@ -134,7 +138,7 @@ class WithHeldTests : InstrumentedTest {
 
     @Test
     @Ignore("This test will be ignored until it is fixed")
-    fun  test_WithHeldNoOlm() {
+    fun test_WithHeldNoOlm() {
         val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
         val aliceSession = testData.firstSession
         val bobSession = testData.secondSession!!
@@ -165,7 +169,9 @@ class WithHeldTests : InstrumentedTest {
         val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
         try {
             // .. might need to wait a bit for stability?
-            bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
+            testHelper.runBlockingTest {
+                bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
+            }
             Assert.fail("This session should not be able to decrypt")
         } catch (failure: Throwable) {
             val type = (failure as MXCryptoError.Base).errorType
@@ -233,7 +239,11 @@ class WithHeldTests : InstrumentedTest {
             testHelper.retryPeriodicallyWithLatch(latch) {
                 val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
                     // try to decrypt and force key request
-                    tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
+                    tryOrNull {
+                        testHelper.runBlockingTest {
+                            bobSecondSession.cryptoService().decryptEvent(it.root, "")
+                        }
+                    }
                 }
                 sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
                 timeLineEvent != null
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
index 35c5a4dab9cf72b0b99f6c3a88978f11b8bfc1f5..2c96568102904016eafb7439c897c9cf87011eee 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import org.amshove.kluent.shouldBe
 import org.junit.FixMethodOrder
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -39,6 +40,7 @@ import kotlin.coroutines.resume
 
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
+@Ignore("This test is flaky ; see issue #5449")
 class VerificationTest : InstrumentedTest {
 
     data class ExpectedResult(
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
index 9856ee777068bc07c84dda507f299eef25f13845..1e3512a9df59e0d450729036c5761722cd3f7f73 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
@@ -60,7 +60,9 @@ class MarkdownParserTest : InstrumentedTest {
                                     applicationFlavor = "TestFlavor",
                                     roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider()
                             )
-                    ))
+                    ),
+                    TestPermalinkService()
+            )
     )
 
     @Test
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2f9a5e0a73a656f0f00dfdf048fafd85b5e252d2
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.room.send
+
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.HTML
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
+
+class TestPermalinkService : PermalinkService {
+    override fun createPermalink(event: Event, forceMatrixTo: Boolean): String? {
+        return null
+    }
+
+    override fun createPermalink(id: String, forceMatrixTo: Boolean): String? {
+        return ""
+    }
+
+    override fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean): String {
+        return ""
+    }
+
+    override fun createRoomPermalink(roomId: String, viaServers: List<String>?, forceMatrixTo: Boolean): String? {
+        return null
+    }
+
+    override fun getLinkedId(url: String): String? {
+        return null
+    }
+
+    override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
+        return when (type) {
+            HTML     -> "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>"
+            MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)"
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
index 69ae57e644f6d5b79559ee7f97e50135b9c00c96..5c011c8b2fb195711e3afe55bc2f31b389912573 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
@@ -62,7 +62,11 @@ internal class ChunkEntityTest : InstrumentedTest {
             val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
                 realm.copyToRealm(it)
             }
-            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
+            chunk.addTimelineEvent(
+                    roomId = ROOM_ID,
+                    eventEntity = fakeEvent,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = emptyMap())
             chunk.timelineEvents.size shouldBeEqualTo 1
         }
     }
@@ -74,8 +78,16 @@ internal class ChunkEntityTest : InstrumentedTest {
             val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
                 realm.copyToRealm(it)
             }
-            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
-            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
+            chunk.addTimelineEvent(
+                    roomId = ROOM_ID,
+                    eventEntity = fakeEvent,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = emptyMap())
+            chunk.addTimelineEvent(
+                    roomId = ROOM_ID,
+                    eventEntity = fakeEvent,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = emptyMap())
             chunk.timelineEvents.size shouldBeEqualTo 1
         }
     }
@@ -144,7 +156,11 @@ internal class ChunkEntityTest : InstrumentedTest {
             val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let {
                 realm.copyToRealm(it)
             }
-            addTimelineEvent(roomId, fakeEvent, direction, emptyMap())
+            addTimelineEvent(
+                    roomId = roomId,
+                    eventEntity = fakeEvent,
+                    direction = direction,
+                    roomMemberContentsByUser = emptyMap())
         }
     }
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6792d6ddfd039024870c2434b36cd2efb32b3399
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.timeline
+
+import org.amshove.kluent.fail
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeGreaterThan
+import org.amshove.kluent.shouldContain
+import org.amshove.kluent.shouldContainAll
+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.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary
+import org.matrix.android.sdk.api.session.room.model.PollSummaryContent
+import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
+import org.matrix.android.sdk.api.session.room.model.message.PollType
+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.common.CommonTestHelper
+import org.matrix.android.sdk.common.CryptoTestHelper
+import java.util.concurrent.CountDownLatch
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class PollAggregationTest : InstrumentedTest {
+
+    @Test
+    fun testAllPollUseCases() {
+        val commonTestHelper = CommonTestHelper(context())
+        val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+        val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
+
+        val roomFromBobPOV = cryptoTestData.secondSession!!.getRoom(cryptoTestData.roomId)!!
+        // Bob creates a poll
+        roomFromBobPOV.sendPoll(PollType.DISCLOSED, pollQuestion, pollOptions)
+
+        aliceSession.startSync(true)
+        val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30))
+        aliceTimeline.start()
+
+        val TOTAL_TEST_COUNT = 7
+        val lock = CountDownLatch(TOTAL_TEST_COUNT)
+
+        val aliceEventsListener = object : Timeline.Listener {
+            override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
+                snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START }?.let { pollEvent ->
+                    val pollEventId = pollEvent.eventId
+                    val pollContent = pollEvent.root.content?.toModel<MessagePollContent>()
+                    val pollSummary = pollEvent.annotations?.pollResponseSummary
+
+                    if (pollContent == null) {
+                        fail("Poll content is null")
+                        return
+                    }
+
+                    when (lock.count.toInt()) {
+                        TOTAL_TEST_COUNT     -> {
+                            // Poll has just been created.
+                            testInitialPollConditions(pollContent, pollSummary)
+                            lock.countDown()
+                            roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
+                        }
+                        TOTAL_TEST_COUNT - 1 -> {
+                            // Bob: Option 1
+                            testBobVotesOption1(pollContent, pollSummary)
+                            lock.countDown()
+                            roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
+                        }
+                        TOTAL_TEST_COUNT - 2 -> {
+                            // Bob: Option 2
+                            testBobChangesVoteToOption2(pollContent, pollSummary)
+                            lock.countDown()
+                            roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
+                        }
+                        TOTAL_TEST_COUNT - 3 -> {
+                            // Alice: Option 2, Bob: Option 2
+                            testAliceAndBobVoteToOption2(pollContent, pollSummary)
+                            lock.countDown()
+                            roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
+                        }
+                        TOTAL_TEST_COUNT - 4 -> {
+                            // Alice: Option 1, Bob: Option 2
+                            testAliceVotesOption1AndBobVotesOption2(pollContent, pollSummary)
+                            lock.countDown()
+                            roomFromBobPOV.endPoll(pollEventId)
+                        }
+                        TOTAL_TEST_COUNT - 5 -> {
+                            // Alice: Option 1, Bob: Option 2 [poll is ended]
+                            testEndedPoll(pollSummary)
+                            lock.countDown()
+                            roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
+                        }
+                        TOTAL_TEST_COUNT - 6 -> {
+                            // Alice: Option 1 (ignore change), Bob: Option 2 [poll is ended]
+                            testAliceVotesOption1AndBobVotesOption2(pollContent, pollSummary)
+                            testEndedPoll(pollSummary)
+                            lock.countDown()
+                        }
+                        else                 -> {
+                            fail("Lock count ${lock.count} didn't handled.")
+                        }
+                    }
+                }
+            }
+        }
+
+        aliceTimeline.addListener(aliceEventsListener)
+
+        commonTestHelper.await(lock)
+
+        aliceTimeline.removeAllListeners()
+
+        aliceSession.stopSync()
+        aliceTimeline.dispose()
+        cryptoTestData.cleanUp(commonTestHelper)
+    }
+
+    private fun testInitialPollConditions(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+        // No votes yet, poll summary should be null
+        pollSummary shouldBe null
+        // Question should be the same as intended
+        pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() shouldBeEqualTo pollQuestion
+        // Options should be the same as intended
+        pollContent.getBestPollCreationInfo()?.answers?.let { answers ->
+            answers.size shouldBeEqualTo pollOptions.size
+            answers.map { it.getBestAnswer() } shouldContainAll pollOptions
+        }
+    }
+
+    private fun testBobVotesOption1(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+        if (pollSummary == null) {
+            fail("Poll summary shouldn't be null when someone votes")
+            return
+        }
+        val answerId = pollContent.getBestPollCreationInfo()?.answers?.first()?.id
+        // Check if the intended vote is in poll summary
+        pollSummary.aggregatedContent?.let { aggregatedContent ->
+            assertTotalVotesCount(aggregatedContent, 1)
+            aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
+            aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 1
+            aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
+        } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+    }
+
+    private fun testBobChangesVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+        if (pollSummary == null) {
+            fail("Poll summary shouldn't be null when someone votes")
+            return
+        }
+        val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
+        // Check if the intended vote is in poll summary
+        pollSummary.aggregatedContent?.let { aggregatedContent ->
+            assertTotalVotesCount(aggregatedContent, 1)
+            aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
+            aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 1
+            aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
+        } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+    }
+
+    private fun testAliceAndBobVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+        if (pollSummary == null) {
+            fail("Poll summary shouldn't be null when someone votes")
+            return
+        }
+        val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
+        // Check if the intended votes is in poll summary
+        pollSummary.aggregatedContent?.let { aggregatedContent ->
+            assertTotalVotesCount(aggregatedContent, 2)
+            aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
+            aggregatedContent.votes?.get(1)?.option shouldBeEqualTo answerId
+            aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 2
+            aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
+        } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+    }
+
+    private fun testAliceVotesOption1AndBobVotesOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+        if (pollSummary == null) {
+            fail("Poll summary shouldn't be null when someone votes")
+            return
+        }
+        val firstAnswerId = pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id
+        val secondAnswerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
+        // Check if the intended votes is in poll summary
+        pollSummary.aggregatedContent?.let { aggregatedContent ->
+            assertTotalVotesCount(aggregatedContent, 2)
+            aggregatedContent.votes!!.map { it.option } shouldContain firstAnswerId
+            aggregatedContent.votes!!.map { it.option } shouldContain secondAnswerId
+            aggregatedContent.votesSummary?.get(firstAnswerId)?.total shouldBeEqualTo 1
+            aggregatedContent.votesSummary?.get(secondAnswerId)?.total shouldBeEqualTo 1
+            aggregatedContent.votesSummary?.get(firstAnswerId)?.percentage shouldBeEqualTo 0.5
+            aggregatedContent.votesSummary?.get(secondAnswerId)?.percentage shouldBeEqualTo 0.5
+        } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+    }
+
+    private fun testEndedPoll(pollSummary: PollResponseAggregatedSummary?) {
+        pollSummary?.closedTime ?: 0 shouldBeGreaterThan 0
+    }
+
+    private fun assertTotalVotesCount(aggregatedContent: PollSummaryContent, expectedVoteCount: Int) {
+        aggregatedContent.totalVotes shouldBeEqualTo expectedVoteCount
+        aggregatedContent.votes?.size shouldBeEqualTo expectedVoteCount
+    }
+
+    companion object {
+        const val pollQuestion = "Do you like creating polls?"
+        val pollOptions = listOf("Yes", "Absolutely", "As long as tests pass")
+    }
+}
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 c87f21d7ac3a5b1d6e475c855e2ab083848a085e..c4bc289b7532ee56802a8b2be2747996db68ede1 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
@@ -60,7 +60,15 @@ data class MatrixConfiguration(
         /**
          * RoomDisplayNameFallbackProvider to provide default room display name.
          */
-        val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider
+        val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider,
+        /**
+         * True to enable presence information sync (if available). False to disable regardless of server setting.
+         */
+        val presenceSyncEnabled: Boolean = true,
+        /**
+         * Thread messages default enable/disabled value
+         */
+        val threadMessagesEnabledDefault: Boolean = false,
 ) {
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
index aabe6e0d0698c37280687c39034cd9f9f2dce055..89b4a343dd4fd93b6d8338c673acd5588395104b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
@@ -58,12 +58,36 @@ fun Throwable.getRetryDelay(defaultValue: Long): Long {
             ?: defaultValue
 }
 
+fun Throwable.isUsernameInUse(): Boolean {
+    return this is Failure.ServerError && error.code == MatrixError.M_USER_IN_USE
+}
+
+fun Throwable.isInvalidUsername(): Boolean {
+    return this is Failure.ServerError &&
+            error.code == MatrixError.M_INVALID_USERNAME
+}
+
 fun Throwable.isInvalidPassword(): Boolean {
     return this is Failure.ServerError &&
             error.code == MatrixError.M_FORBIDDEN &&
             error.message == "Invalid password"
 }
 
+fun Throwable.isRegistrationDisabled(): Boolean {
+    return this is Failure.ServerError && error.code == MatrixError.M_FORBIDDEN &&
+            httpCode == HttpsURLConnection.HTTP_FORBIDDEN
+}
+
+fun Throwable.isWeakPassword(): Boolean {
+    return this is Failure.ServerError && error.code == MatrixError.M_WEAK_PASSWORD
+}
+
+fun Throwable.isLoginEmailUnknown(): Boolean {
+    return this is Failure.ServerError &&
+            error.code == MatrixError.M_FORBIDDEN &&
+            error.message.isEmpty()
+}
+
 fun Throwable.isInvalidUIAAuth(): Boolean {
     return this is Failure.ServerError &&
             error.code == MatrixError.M_FORBIDDEN &&
@@ -104,8 +128,8 @@ fun Throwable.isRegistrationAvailabilityError(): Boolean {
     return this is Failure.ServerError &&
             httpCode == HttpsURLConnection.HTTP_BAD_REQUEST && /* 400 */
             (error.code == MatrixError.M_USER_IN_USE ||
-            error.code == MatrixError.M_INVALID_USERNAME ||
-            error.code == MatrixError.M_EXCLUSIVE)
+                    error.code == MatrixError.M_INVALID_USERNAME ||
+                    error.code == MatrixError.M_EXCLUSIVE)
 }
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
index e3f00a24b669c6aebad3ff375700e8c26f37506f..a5b442dc4a71c991c678b7ba877326c8c291c8ed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
@@ -121,7 +121,7 @@ interface CryptoService {
     fun discardOutboundSession(roomId: String)
 
     @Throws(MXCryptoError::class)
-    fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
+    suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
 
     fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>)
 
@@ -140,7 +140,6 @@ interface CryptoService {
     fun getLiveCryptoDeviceInfo(userIds: List<String>): LiveData<List<CryptoDeviceInfo>>
 
     fun addNewSessionListener(newSessionListener: NewSessionListener)
-
     fun removeSessionListener(listener: NewSessionListener)
 
     fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
index 34096d603fd14b877e4a7ee9811680a2267a717d..ae8ed3941fa3d4396044186eab396204a293791d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
@@ -49,5 +49,6 @@ import com.squareup.moshi.JsonClass
 @JsonClass(generateAdapter = true)
 data class AggregatedRelations(
         @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
-        @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null
+        @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
+        @Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index df57ca568111d9c5144ee77e168d440dfd8a96f3..f1304f62163755324cb4da42679f1ab9d41d7d86 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -201,7 +201,11 @@ data class Event(
      */
     fun getDecryptedTextSummary(): String? {
         if (isRedacted()) return "Message Deleted"
-        val text = getDecryptedValue() ?: return null
+        val text = getDecryptedValue() ?: run {
+            if (isPoll()) { return getPollQuestion() ?: "created a poll." }
+            return null
+        }
+
         return when {
             isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
             isFileMessage()                        -> "sent a file."
@@ -349,7 +353,7 @@ fun Event.isAttachmentMessage(): Boolean {
             }
 }
 
-fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END
+fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END
 
 fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
 
@@ -372,7 +376,7 @@ fun Event.getRelationContent(): RelationDefaultContent? {
  * Returns the poll question or null otherwise
  */
 fun Event.getPollQuestion(): String? =
-        getPollContent()?.pollCreationInfo?.question?.question
+        getPollContent()?.getBestPollCreationInfo()?.question?.getBestQuestion()
 
 /**
  * Returns the relation content for a specific type or null otherwise
@@ -385,12 +389,12 @@ fun Event.isReply(): Boolean {
 }
 
 fun Event.isReplyRenderedInThread(): Boolean {
-    return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true
+    return isReply() && getRelationContent()?.shouldRenderInThread() == true
 }
 
-fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
+fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null
 
-fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId
+fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId
 
 fun Event.isEdition(): Boolean {
     return getRelationContentForType(RelationType.REPLACE)?.eventId != null
@@ -406,3 +410,5 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
 fun Event.getPollContent(): MessagePollContent? {
     return content.toModel<MessagePollContent>()
 }
+
+fun Event.supportsNotification() = this.getClearType() in EventType.MESSAGE + EventType.POLL_START
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
index 0c77b574e7266dbc71a36c9f615428c3fbed0ea2..855801e79ec0761466c6d2a8771f6c5cc8c65b8d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
@@ -49,6 +49,7 @@ object EventType {
     const val STATE_ROOM_JOIN_RULES = "m.room.join_rules"
     const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
     const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
+    private const val STATE_ROOM_BEACON_INFO_PREFIX = "org.matrix.msc3489.beacon_info."
 
     const val STATE_SPACE_CHILD = "m.space.child"
 
@@ -103,9 +104,9 @@ object EventType {
     const val REACTION = "m.reaction"
 
     // Poll
-    const val POLL_START = "org.matrix.msc3381.poll.start"
-    const val POLL_RESPONSE = "org.matrix.msc3381.poll.response"
-    const val POLL_END = "org.matrix.msc3381.poll.end"
+    val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start")
+    val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response")
+    val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end")
 
     // Unwedging
     internal const val DUMMY = "m.dummy"
@@ -120,4 +121,12 @@ object EventType {
                 type == CALL_REJECT ||
                 type == CALL_REPLACES
     }
+
+    /**
+     * Returns an event type like org.matrix.msc3489.beacon_info.@userid:matrix.org.1648814272273
+     */
+    fun generateBeaconInfoStateEventType(userId: String): String {
+        val uniqueId = System.currentTimeMillis()
+        return "$STATE_ROOM_BEACON_INFO_PREFIX$userId.$uniqueId"
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cc52dfc02c1a4cf67cf5e4996d098a59f0cb5e0e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.matrix.android.sdk.api.session.events.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class LatestThreadUnsignedRelation(
+        override val limited: Boolean? = false,
+        override val count: Int? = 0,
+        @Json(name = "latest_event")
+        val event: Event? = null,
+        @Json(name = "current_user_participated")
+        val isUserParticipating: Boolean? = false
+
+) : UnsignedRelationInfo
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
index fb26264ad7708992935a5fe0dbdefb363af30964..74dc74b294964e2669c558405f2bec33c59eb386 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt
@@ -30,7 +30,6 @@ object RelationType {
 
     /** Lets you define an event which is a thread reply to an existing event.*/
     const val THREAD = "m.thread"
-    const val IO_THREAD = "io.element.thread"
 
     /** Lets you define an event which adds a response to an existing event.*/
     const val RESPONSE = "org.matrix.response"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt
index dfe1db7b1c272a1f74d474b03efc5452875f4775..630a2fb91a538473d788df6e556c18aa573b3522 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt
@@ -46,3 +46,5 @@ data class UnsignedData(
         @Json(name = "replaces_state") val replacesState: String? = null
 
 )
+
+fun UnsignedData?.isRedacted() = this?.redactedEvent != null
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 2256dfb8f0c52d0168daebf7e03727e90a72f7ab..9db3876b7472483f1ae712de97fb55fabc7d855d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
@@ -50,7 +50,11 @@ data class HomeServerCapabilities(
          * This capability describes the default and available room versions a server supports, and at what level of stability.
          * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
          */
-        val roomVersions: RoomVersionCapabilities? = null
+        val roomVersions: RoomVersionCapabilities? = null,
+        /**
+         * True if the home server support threading
+         */
+        var canUseThreading: Boolean = false
 ) {
 
     enum class RoomCapabilitySupport {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
index 920dc85c7a066d1d5a02381d3bf8b906a79d596c..c139da813ab75484865f9540314de811e48b718a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
@@ -28,6 +28,11 @@ interface PermalinkService {
         const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
     }
 
+    enum class SpanTemplateType {
+        HTML,
+        MARKDOWN
+    }
+
     /**
      * Creates a permalink for an event.
      * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org"
@@ -80,4 +85,15 @@ interface PermalinkService {
      * @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink
      */
     fun getLinkedId(url: String): String?
+
+    /**
+     * Creates a HTML or Markdown mention span template. Can be used to replace a mention with a permalink to mentioned user.
+     * Ex: "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>" or "[%2\$s](https://matrix.to/#/%1\$s)"
+     *
+     * @param type: type of template to create
+     * @param forceMatrixTo whether we should force using matrix.to base URL
+     *
+     * @return the created template
+     */
+    fun createMentionSpanTemplate(type: SpanTemplateType, forceMatrixTo: Boolean = false): String
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
index 05fa24946a3fe21f61cfb993f5662e9df48ea29d..d2c677bb31425be809668548914608b3334a425a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
@@ -21,6 +21,7 @@ import android.net.Uri
 import androidx.lifecycle.LiveData
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.session.identity.ThreePid
+import org.matrix.android.sdk.api.session.user.model.User
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.api.util.Optional
 
@@ -118,4 +119,17 @@ interface ProfileService {
      * Remove a 3Pid from the Matrix account.
      */
     suspend fun deleteThreePid(threePid: ThreePid)
+
+    /**
+     * Return a User object from a userId
+     */
+    suspend fun getProfileAsUser(userId: String): User {
+        return getProfile(userId).let { dict ->
+            User(
+                    userId = userId,
+                    displayName = dict[DISPLAY_NAME_KEY] as? String,
+                    avatarUrl = dict[AVATAR_URL_KEY] as? String
+            )
+        }
+    }
 }
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 d930a5d0fd6cb4d392c2097b8ef7b1afa9bdb141..be65b883b36238509027a6398e61652138232e7f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
@@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
 import org.matrix.android.sdk.api.session.room.state.StateService
 import org.matrix.android.sdk.api.session.room.tags.TagsService
 import org.matrix.android.sdk.api.session.room.threads.ThreadsService
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
 import org.matrix.android.sdk.api.session.room.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.typing.TypingService
 import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -47,6 +48,7 @@ import org.matrix.android.sdk.api.util.Optional
 interface Room :
         TimelineService,
         ThreadsService,
+        ThreadsLocalService,
         SendService,
         DraftService,
         ReadService,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
index bca432320d92d456095240ef912163a392f30680..c1c1a385b5c088a9ba256603d0367207eca2a7c4 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
@@ -216,6 +216,12 @@ interface RoomService {
                                           pagedListConfig: PagedList.Config = defaultPagedListConfig,
                                           sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
 
+    /**
+     * Return a LiveData on the number of rooms
+     * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance.
+     */
+    fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int>
+
     /**
      * TODO Doc
      */
@@ -236,4 +242,12 @@ interface RoomService {
      */
     fun getFlattenRoomSummaryChildrenOfLive(spaceId: String?,
                                             memberships: List<Membership> = Membership.activeMemberships()): LiveData<List<RoomSummary>>
+
+    /**
+     * Refreshes the RoomSummary LatestPreviewContent for the given @param roomId
+     * If the roomId is null, all rooms are updated
+     *
+     * This is useful for refreshing summary content with encrypted messages after receiving new room keys
+     */
+    fun refreshJoinedRoomSummaryPreviews(roomId: String?)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
index b83f57f5ef83ac73626fc8d19118f81eb96eb151..db87f913b93df9975c027dbe93a398905c9c6871 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
@@ -22,10 +22,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
 
 interface UpdatableLivePageResult {
     val livePagedList: LiveData<PagedList<RoomSummary>>
-
-    fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams)
-
     val liveBoundaries: LiveData<ResultBoundaries>
+    var queryParams: RoomSummaryQueryParams
 }
 
 data class ResultBoundaries(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/BeaconInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/BeaconInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..873edc0f1fd3ac2976021ea87351039792b8f3e1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/BeaconInfo.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.model.livelocation
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class BeaconInfo(
+        @Json(name = "description") val description: String? = null,
+        /**
+         * Beacon should be considered as inactive after this timeout as milliseconds.
+         */
+        @Json(name = "timeout") val timeout: Long? = null,
+        /**
+         * Should be set true to start sharing beacon.
+         */
+        @Json(name = "live") val isLive: Boolean? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e08d5b629b54786746b6fe8f959974e2d3e57e6d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.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.api.session.room.model.livelocation
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.room.model.message.LocationAsset
+import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType
+
+@JsonClass(generateAdapter = true)
+data class LiveLocationBeaconContent(
+        /**
+         * Indicates user's intent to share ephemeral location.
+         */
+        @Json(name = "org.matrix.msc3489.beacon_info") val unstableBeaconInfo: BeaconInfo? = null,
+        @Json(name = "m.beacon_info") val beaconInfo: BeaconInfo? = null,
+        /**
+         * Beacon creation timestamp.
+         */
+        @Json(name = "org.matrix.msc3488.ts") val unstableTimestampAsMilliseconds: Long? = null,
+        @Json(name = "m.ts") val timestampAsMilliseconds: Long? = null,
+        /**
+         * Live location asset type.
+         */
+        @Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset = LocationAsset(LocationAssetType.SELF),
+        @Json(name = "m.asset") val locationAsset: LocationAsset? = null
+) {
+
+    fun getBestBeaconInfo() = beaconInfo ?: unstableBeaconInfo
+
+    fun getBestTimestampAsMilliseconds() = timestampAsMilliseconds ?: unstableTimestampAsMilliseconds
+
+    fun getBestLocationAsset() = locationAsset ?: unstableLocationAsset
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
index e8b3cf2488860a0bc6019d80db8844f771069546..35fa555a5bab38c4bf210c6c61dbd4aa6b411c1b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
@@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class LocationAsset(
-        @Json(name = "type") val type: LocationAssetType? = null
+        @Json(name = "type") val type: String? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
index ef40e21c47f582889fd015801131a937be268e20..f7d82d4b40909d1f59e3637e128a417191ecc883 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
@@ -16,11 +16,20 @@
 
 package org.matrix.android.sdk.api.session.room.model.message
 
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
+/**
+ * Define what particular asset is being referred to.
+ * We don't use enum type since it is not limited to a specific set of values.
+ * The way this type should be interpreted in client side is described in
+ * [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
+ */
+object LocationAssetType {
+    /**
+     * Used for user location sharing.
+     **/
+    const val SELF = "m.self"
 
-@JsonClass(generateAdapter = false)
-enum class LocationAssetType {
-    @Json(name = "m.self")
-    SELF
+    /**
+     * Used for pin drop location sharing.
+     **/
+    const val PIN = "m.pin"
 }
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 d07bd2d73ae5df8f6f8ad1e853f11881cbd26636..2052133b068cf360d7da16ae60ae7fb4f758edce 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
@@ -39,37 +39,47 @@ data class MessageLocationContent(
          */
         @Json(name = "geo_uri") val geoUri: String,
 
-        /**
-         * See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
-         */
-        @Json(name = "org.matrix.msc3488.location") val locationInfo: LocationInfo? = null,
-
         @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
         @Json(name = "m.new_content") override val newContent: Content? = null,
-
         /**
-         * m.asset defines a generic asset that can be used for location tracking but also in other places like
+         * See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
+         */
+        @Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
+        @Json(name = "m.location") val locationInfo: LocationInfo? = null,
+        /**
+         * Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
+         */
+        @Json(name = "org.matrix.msc3488.ts") val unstableTs: Long? = null,
+        @Json(name = "m.ts") val ts: Long? = null,
+        @Json(name = "org.matrix.msc1767.text") val unstableText: String? = null,
+        @Json(name = "m.text") val text: String? = null,
+        /**
+         * Defines a generic asset that can be used for location tracking but also in other places like
          * inventories, geofencing, checkins/checkouts etc.
          * It should contain a mandatory namespaced type key defining what particular asset is being referred to.
          * For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
+         * See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
          */
-        @Json(name = "m.asset") val locationAsset: LocationAsset? = null,
+        @Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null,
+        @Json(name = "m.asset") val locationAsset: LocationAsset? = null
+) : MessageContent {
 
-        /**
-         * Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
-         */
-        @Json(name = "org.matrix.msc3488.ts") val ts: Long? = null,
+    fun getBestLocationInfo() = locationInfo ?: unstableLocationInfo
 
-        @Json(name = "org.matrix.msc1767.text") val text: String? = null
-) : MessageContent {
+    fun getBestTs() = ts ?: unstableTs
+
+    fun getBestText() = text ?: unstableText
+
+    fun getBestLocationAsset() = locationAsset ?: unstableLocationAsset
 
-    fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
+    fun getBestGeoUri() = getBestLocationInfo()?.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
+        val locationAsset = getBestLocationAsset()
         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/message/MessagePollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
index a4e131729066227a6655dd9f364ee333660ec94a..43c0c9006818342e9c4f531936df68299eba1688 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
@@ -31,5 +31,9 @@ data class MessagePollContent(
         @Json(name = "body") override val body: String = "",
         @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
         @Json(name = "m.new_content") override val newContent: Content? = null,
-        @Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
-) : MessageContent
+        @Json(name = "org.matrix.msc3381.poll.start") val unstablePollCreationInfo: PollCreationInfo? = null,
+        @Json(name = "m.poll.start") val pollCreationInfo: PollCreationInfo? = null
+) : MessageContent {
+
+    fun getBestPollCreationInfo() = pollCreationInfo ?: unstablePollCreationInfo
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt
index f3b4e3dc23c1d1e8f0b9b1cdce9e59ed7f0ee415..022915ed695d1a4cc1d88c4447f57333d0b436b3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt
@@ -31,5 +31,9 @@ data class MessagePollResponseContent(
         @Json(name = "body") override val body: String = "",
         @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
         @Json(name = "m.new_content") override val newContent: Content? = null,
-        @Json(name = "org.matrix.msc3381.poll.response") val response: PollResponse? = null
-) : MessageContent
+        @Json(name = "org.matrix.msc3381.poll.response") val unstableResponse: PollResponse? = null,
+        @Json(name = "m.response") val response: PollResponse? = null
+) : MessageContent {
+
+    fun getBestResponse() = response ?: unstableResponse
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
index 8f5ff53c85cf8329e996c7d400a895327563701f..34614d9d15d6b066a83740435bb74ef973ce07f0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
@@ -22,5 +22,9 @@ import com.squareup.moshi.JsonClass
 @JsonClass(generateAdapter = true)
 data class PollAnswer(
         @Json(name = "id") val id: String? = null,
-        @Json(name = "org.matrix.msc1767.text") val answer: String? = null
-)
+        @Json(name = "org.matrix.msc1767.text") val unstableAnswer: String? = null,
+        @Json(name = "m.text") val answer: String? = null
+) {
+
+    fun getBestAnswer() = answer ?: unstableAnswer
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
index a82c01b15989bbd5425a3e89085ec18d108fd81a..81b034a809b3debb3dd2faf8d13b241b0ae36c6b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
@@ -21,8 +21,8 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class PollCreationInfo(
-    @Json(name = "question") val question: PollQuestion? = null,
-    @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED,
-    @Json(name = "max_selections") val maxSelections: Int = 1,
-    @Json(name = "answers") val answers: List<PollAnswer>? = null
+        @Json(name = "question") val question: PollQuestion? = null,
+        @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE,
+        @Json(name = "max_selections") val maxSelections: Int = 1,
+        @Json(name = "answers") val answers: List<PollAnswer>? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
index 76025f745e6346cac622dbca8a429c2a530ab8d3..df9517892b18e2c9ee11f969ab2aba16d7b4264d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
@@ -21,5 +21,9 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class PollQuestion(
-        @Json(name = "org.matrix.msc1767.text") val question: String? = null
-)
+        @Json(name = "org.matrix.msc1767.text") val unstableQuestion: String? = null,
+        @Json(name = "m.text") val question: String? = null
+) {
+
+    fun getBestQuestion() = question ?: unstableQuestion
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt
index 3a8066b9bc10caea1f2ff1893ee070eb360a149a..54801e698dbdb3a25e8f9c0e0461d614c92c654a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt
@@ -25,11 +25,17 @@ enum class PollType {
      * Voters should see results as soon as they have voted.
      */
     @Json(name = "org.matrix.msc3381.poll.disclosed")
+    DISCLOSED_UNSTABLE,
+
+    @Json(name = "m.poll.disclosed")
     DISCLOSED,
 
     /**
      * Results should be only revealed when the poll is ended.
      */
     @Json(name = "org.matrix.msc3381.poll.undisclosed")
+    UNDISCLOSED_UNSTABLE,
+
+    @Json(name = "m.poll.undisclosed")
     UNDISCLOSED
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
index 733d6c37e86d977c997e6d48c9b58682c0979cef..e7bebeeff66afa5e429186f6658875f381ff58f8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
@@ -26,5 +26,6 @@ data class ReactionInfo(
         @Json(name = "key") val key: String,
         // always null for reaction
         @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
-        @Json(name = "option") override val option: Int? = null
+        @Json(name = "option") override val option: Int? = null,
+        @Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
 ) : RelationContent
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt
index e2080bb4376fb24dcd07bd7555d6330415ceb93e..53b1fea873d5b43dd936a7b88c74dd528e9246b0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt
@@ -24,4 +24,10 @@ interface RelationContent {
     val eventId: String?
     val inReplyTo: ReplyToContent?
     val option: Int?
+
+    /**
+     * This flag indicates that the message should be rendered as a reply
+     * fallback, when isFallingBack = false
+     */
+    val isFallingBack: Boolean?
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
index 10b071a6013cc58a63fcea6e1e48d1db10ac05ec..5dcb1b4323bcf1d2edc803dafdae8cf0750d2971 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
@@ -23,5 +23,8 @@ data class RelationDefaultContent(
         @Json(name = "rel_type") override val type: String?,
         @Json(name = "event_id") override val eventId: String?,
         @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
-        @Json(name = "option") override val option: Int? = null
+        @Json(name = "option") override val option: Int? = null,
+        @Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
 ) : RelationContent
+
+fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
index 09114436f04075cd9401717c4af0bd6a39e7961b..44098989084eb4d43435fc9f65960a93bd7ab0e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
@@ -163,13 +163,4 @@ interface RelationService {
                       autoMarkdown: Boolean = false,
                       formattedText: String? = null,
                       eventReplied: TimelineEvent? = null): Cancelable?
-
-    /**
-     * Get all the thread replies for the specified rootThreadEventId
-     * The return list will contain the original root thread event and all the thread replies to that event
-     * Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready
-     * from the backend
-     * @param rootThreadEventId the root thread eventId
-     */
-    suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
index 412a1bfca9dcec11ab88a5d97520cc8f4aecc974..251328bea210065be3c79c58633453f935899f78 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
@@ -21,8 +21,5 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class ReplyToContent(
-        @Json(name = "event_id") val eventId: String? = null,
-        @Json(name = "render_in") val renderIn: List<String>? = null
+        @Json(name = "event_id") val eventId: String? = null
 )
-
-fun ReplyToContent.shouldRenderInThread(): Boolean = renderIn?.contains("m.thread") == true
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index 913dbfd01096adf5f93ffbaa3751e4fd705a6206..9f8b1d93d71f38f5a46c018132c371cfbef2879b 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
@@ -142,8 +142,9 @@ interface SendService {
      * @param latitude required latitude of the location
      * @param longitude required longitude of the location
      * @param uncertainty Accuracy of the location in meters
+     * @param isUserLocation indicates whether the location data corresponds to the user location or not
      */
-    fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
+    fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
 
     /**
      * Remove this failed message from the timeline
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
index 3bba2deae53019430464d9c80d81f1aad9186c7a..eaed9053eaed84a8f175022ff54ddacf32efd3de 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
@@ -32,7 +32,6 @@ object RoomSummaryConstants {
             EventType.CALL_ANSWER,
             EventType.ENCRYPTED,
             EventType.STICKER,
-            EventType.REACTION,
-            EventType.POLL_START
-    )
+            EventType.REACTION
+    ) + EventType.POLL_START
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
index e4d1d979e1a8f8c61b55fa8afc5a99b58ee8a040..839cdff63badd9cb2052cb615e2ffec14f5d1875 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
@@ -17,51 +17,43 @@
 package org.matrix.android.sdk.api.session.room.threads
 
 import androidx.lifecycle.LiveData
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
 
 /**
- * This interface defines methods to interact with threads related features.
- * It's implemented at the room level within the main timeline.
+ * This interface defines methods to interact with thread related features.
+ * It's the dynamic threads implementation and the homeserver must return
+ * a capability entry for threads. If the server do not support m.thread
+ * then [ThreadsLocalService] should be used instead
  */
 interface ThreadsService {
 
     /**
-     * Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
+     * Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level
      */
-    fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
+    fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>>
 
     /**
-     * Returns a list of all the thread root TimelineEvents that exists at the room level
+     * Returns a list of all the [ThreadSummary] that exists at the room level
      */
-    fun getAllThreads(): List<TimelineEvent>
+    fun getAllThreadSummaries(): List<ThreadSummary>
 
     /**
-     * Returns a [LiveData] list of all the marked unread threads that exists at the room level
-     */
-    fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
-
-    /**
-     * Returns a list of all the marked unread threads that exists at the room level
-     */
-    fun getMarkedThreadNotifications(): List<TimelineEvent>
-
-    /**
-     * Returns whether or not the current user is participating in the thread
-     * @param rootThreadEventId the eventId of the current thread
+     * Enhance the provided ThreadSummary[List] by adding the latest
+     * message edition for that thread
+     * @return the enhanced [List] with edited updates
      */
-    fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
+    fun enhanceThreadWithEditions(threads: List<ThreadSummary>): List<ThreadSummary>
 
     /**
-     * Enhance the provided root thread TimelineEvent [List] by adding the latest
-     * message edition for that thread
-     * @return the enhanced [List] with edited updates
+     * Fetch all thread replies for the specified thread using the /relations api
+     * @param rootThreadEventId the root thread eventId
+     * @param from defines the token that will fetch from that position
+     * @param limit defines the number of max results the api will respond with
      */
-    fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
+    suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int)
 
     /**
-     * Marks the current thread as read in local DB.
-     * note: read receipts within threads are not yet supported with the API
-     * @param rootThreadEventId the root eventId of the current thread
+     * Fetch all thread summaries for the current room using the enhanced /messages api
      */
-    suspend fun markThreadAsRead(rootThreadEventId: String)
+    suspend fun fetchThreadSummaries()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f7b379e3821b04d1a920b993139b0a647c8a6757
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room.threads.local
+
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+
+/**
+ * This interface defines methods to interact with thread related features.
+ * It's the local threads implementation and assumes that the homeserver
+ * do not support threads
+ */
+interface ThreadsLocalService {
+
+    /**
+     * Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
+     */
+    fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
+
+    /**
+     * Returns a list of all the thread root TimelineEvents that exists at the room level
+     */
+    fun getAllThreads(): List<TimelineEvent>
+
+    /**
+     * Returns a [LiveData] list of all the marked unread threads that exists at the room level
+     */
+    fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
+
+    /**
+     * Returns a list of all the marked unread threads that exists at the room level
+     */
+    fun getMarkedThreadNotifications(): List<TimelineEvent>
+
+    /**
+     * Returns whether or not the current user is participating in the thread
+     * @param rootThreadEventId the eventId of the current thread
+     */
+    fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
+
+    /**
+     * Enhance the provided root thread TimelineEvent [List] by adding the latest
+     * message edition for that thread
+     * @return the enhanced [List] with edited updates
+     */
+    fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
+
+    /**
+     * Marks the current thread as read in local DB.
+     * note: read receipts within threads are not yet supported with the API
+     * @param rootThreadEventId the root eventId of the current thread
+     */
+    suspend fun markThreadAsRead(rootThreadEventId: String)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c8353cf0de0c7255aae7472e55b0eda99653eb3c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 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.model
+
+data class ThreadEditions(var rootThreadEdition: String? = null,
+                          var latestThreadEdition: String? = null)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
new file mode 100644
index 0000000000000000000000000000000000000000..017afba1baebee3b169a672542515b4ea55a81b8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 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.model
+
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+
+/**
+ * The main thread Summary model, mainly used to display the thread list
+ */
+data class ThreadSummary(val roomId: String,
+                         val rootEvent: Event?,
+                         val latestEvent: Event?,
+                         val rootEventId: String,
+                         val rootThreadSenderInfo: SenderInfo,
+                         val latestThreadSenderInfo: SenderInfo,
+                         val isUserParticipating: Boolean,
+                         val numberOfThreads: Int,
+                         val threadEditions: ThreadEditions = ThreadEditions())
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
similarity index 68%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
index 097bdaf153069e06e7ac31ceb2a1707fe5f45149..95697f987f08b3c3fabfaebe10f77b5fae4e1b75 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
@@ -1,11 +1,11 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.util
+package org.matrix.android.sdk.api.session.room.threads.model
 
-// Trick to ensure that when block is exhaustive
-internal val <T> T.exhaustive: T get() = this
+enum class ThreadSummaryUpdateType {
+    REPLACE,
+    ADD
+}
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 6f8bae876bded065bf675a015ee7b44bc3d1dfc4..1b01efc074b4605a38fdf66a45afc7291209518b 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
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.api.session.room.timeline
 
 import org.matrix.android.sdk.BuildConfig
+import org.matrix.android.sdk.api.extensions.orFalse
 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
@@ -54,6 +55,7 @@ data class TimelineEvent(
          * It's not unique on the timeline as it's reset on each chunk.
          */
         val displayIndex: Int,
+        var ownedByThreadChunk: Boolean = false,
         val senderInfo: SenderInfo,
         val annotations: EventAnnotationsSummary? = null,
         val readReceipts: List<ReadReceipt> = emptyList()
@@ -134,9 +136,9 @@ fun TimelineEvent.getEditedEventId(): String? {
  */
 fun TimelineEvent.getLastMessageContent(): MessageContent? {
     return when (root.getClearType()) {
-        EventType.STICKER    -> root.getClearContent().toModel<MessageStickerContent>()
-        EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
-        else                 -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
+        EventType.STICKER       -> root.getClearContent().toModel<MessageStickerContent>()
+        in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
+        else                    -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
     }
 }
 
@@ -158,6 +160,13 @@ fun TimelineEvent.isSticker(): Boolean {
     return root.isSticker()
 }
 
+/**
+ * Returns whether or not the event is a root thread event
+ */
+fun TimelineEvent.isRootThread(): Boolean {
+    return root.threadDetails?.isRootThread.orFalse()
+}
+
 /**
  * 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/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
index 61520696445913f8dac2b508b3773e0357b5f5fe..46433f387d36b2297d3571c687feadb122a9e2c0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
@@ -38,7 +38,7 @@ interface TimelineService {
 
     /**
      * Returns a snapshot of TimelineEvent event with eventId.
-     * At the opposite of getTimeLineEventLive which will be updated when local echo event is synced, it will return null in this case.
+     * At the opposite of getTimelineEventLive which will be updated when local echo event is synced, it will return null in this case.
      * @param eventId the eventId to get the TimelineEvent
      */
     fun getTimelineEvent(eventId: String): TimelineEvent?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
index fafe17b2c0957261a1992858f89344d1e65b5dac..d6937d5b265b65cc564d85ca295b83a23ee78014 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.api.session.threads
 
+import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.sender.SenderInfo
 
 /**
@@ -26,7 +27,7 @@ data class ThreadDetails(
         val isRootThread: Boolean = false,
         val numberOfThreads: Int = 0,
         val threadSummarySenderInfo: SenderInfo? = null,
-        val threadSummaryLatestTextMessage: String? = null,
+        val threadSummaryLatestEvent: Event? = null,
         val lastMessageTimestamp: Long? = null,
         var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE,
         val isThread: Boolean = false,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
index 302f7387fad7363c72988c1a307f33a13bb4f1f8..650b8cc26db1a5ab7c22f1b72159b1c69564bc65 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.api.util
 
 import org.matrix.android.sdk.BuildConfig
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.group.model.GroupSummary
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
@@ -199,6 +200,8 @@ fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName,
 
 fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)
 
+fun SenderInfo.toMatrixItemOrNull() = tryOrNull {  MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) }
+
 fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) {
     MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl)
 } else {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
index 0a9b8b73cc1c5084f7beb44a78998eb744dc3ab7..815f8de2de28b80c6a1067f943b2e67b3e21395c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
@@ -38,7 +38,7 @@ internal data class HomeServerVersion(
     }
 
     companion object {
-        internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""")
+        internal val pattern = Regex("""[r|v](\d+)\.(\d+)\.(\d+)""")
 
         internal fun parse(value: String): HomeServerVersion? {
             val result = pattern.matchEntire(value) ?: return null
@@ -56,5 +56,6 @@ internal data class HomeServerVersion(
         val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
         val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
         val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
+        val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
index 74cb3de2acf5bddf4af03aedfd8da6398eaca6ef..d07d5ecd64b62ba4eef581183557a9d98404f7bf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
@@ -51,6 +51,8 @@ private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"
 private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
 private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
 private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
+private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
+private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable"
 
 /**
  * Return true if the SDK supports this homeserver version
@@ -68,6 +70,14 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
             doesServerSeparatesAddAndBind()
 }
 
+/**
+ * Indicate if the homeserver support MSC3440 for threads
+ */
+internal fun Versions.doesServerSupportThreads(): Boolean {
+    return getMaxVersion() >= HomeServerVersion.v1_3_0 ||
+            unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
+}
+
 /**
  * Return true if the server support the lazy loading of room members
  *
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt
index 3130a6382f1f8dd0346831f87bb18bd57a9d9d4f..2265526484bd5b734ea2cedeb5c111c953b3b3cc 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
@@ -137,8 +137,7 @@ internal abstract class CryptoModule {
         @JvmStatic
         @Provides
         @CryptoDatabase
-        fun providesClearCacheTask(@CryptoDatabase
-                                   realmConfiguration: RealmConfiguration): ClearCacheTask {
+        fun providesClearCacheTask(@CryptoDatabase realmConfiguration: RealmConfiguration): ClearCacheTask {
             return RealmClearCacheTask(realmConfiguration)
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
index 0646e4d2b8c7e72925209d502fce6c68d1c4add2..db44abc36ff4279bfeb5c951b39a5fb82f511619 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
@@ -434,6 +434,14 @@ internal class DefaultCryptoService @Inject constructor(
                     val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
                     oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
                 }
+
+                // unwedge if needed
+                try {
+                    eventDecryptor.unwedgeDevicesIfNeeded()
+                } catch (failure: Throwable) {
+                    Timber.tag(loggerTag.value).w("unwedgeDevicesIfNeeded failed")
+                }
+
                 // There is a limit of to_device events returned per sync.
                 // If we are in a case of such limited to_device sync we can't try to generate/upload
                 // new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate
@@ -723,7 +731,7 @@ internal class DefaultCryptoService @Inject constructor(
      * @return the MXEventDecryptionResult data, or throw in case of error
      */
     @Throws(MXCryptoError::class)
-    override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         return internalDecryptEvent(event, timeline)
     }
 
@@ -746,7 +754,7 @@ internal class DefaultCryptoService @Inject constructor(
      * @return the MXEventDecryptionResult data, or null in case of error
      */
     @Throws(MXCryptoError::class)
-    private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         return eventDecryptor.decryptEvent(event, timeline)
     }
 
@@ -1364,6 +1372,9 @@ internal class DefaultCryptoService @Inject constructor(
     @VisibleForTesting
     val cryptoStoreForTesting = cryptoStore
 
+    @VisibleForTesting
+    val olmDeviceForTest = olmDevice
+
     companion object {
         const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
index 57381eacfb47954160d19e3e4b8e05618fb85f72..00efd3d6a854ceb5c80815f92ea8fe64aa577208 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
@@ -21,14 +21,13 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
 import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
 import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@@ -40,6 +39,8 @@ import javax.inject.Inject
 
 private const val SEND_TO_DEVICE_RETRY_COUNT = 3
 
+private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
+
 @SessionScope
 internal class EventDecryptor @Inject constructor(
         private val cryptoCoroutineScope: CoroutineScope,
@@ -47,13 +48,22 @@ internal class EventDecryptor @Inject constructor(
         private val roomDecryptorProvider: RoomDecryptorProvider,
         private val messageEncrypter: MessageEncrypter,
         private val sendToDeviceTask: SendToDeviceTask,
+        private val deviceListManager: DeviceListManager,
         private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
         private val cryptoStore: IMXCryptoStore
 ) {
 
-    // The date of the last time we forced establishment
-    // of a new session for each user:device.
-    private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
+    /**
+     * Rate limit unwedge attempt, should we persist that?
+     */
+    private val lastNewSessionForcedDates = mutableMapOf<WedgedDeviceInfo, Long>()
+
+    data class WedgedDeviceInfo(
+            val userId: String,
+            val senderKey: String?
+    )
+
+    private val wedgedDevices = mutableListOf<WedgedDeviceInfo>()
 
     /**
      * Decrypt an event
@@ -63,7 +73,7 @@ internal class EventDecryptor @Inject constructor(
      * @return the MXEventDecryptionResult data, or throw in case of error
      */
     @Throws(MXCryptoError::class)
-    fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         return internalDecryptEvent(event, timeline)
     }
 
@@ -91,38 +101,32 @@ internal class EventDecryptor @Inject constructor(
      * @return the MXEventDecryptionResult data, or null in case of error
      */
     @Throws(MXCryptoError::class)
-    private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         val eventContent = event.content
         if (eventContent == null) {
-            Timber.e("## CRYPTO | decryptEvent : empty event content")
+            Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
         } else {
             val algorithm = eventContent["algorithm"]?.toString()
             val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
             if (alg == null) {
                 val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
-                Timber.e("## CRYPTO | decryptEvent() : $reason")
+                Timber.tag(loggerTag.value).e("decryptEvent() : $reason")
                 throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
             } else {
                 try {
                     return alg.decryptEvent(event, timeline)
                 } catch (mxCryptoError: MXCryptoError) {
-                    Timber.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
+                    Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
                     if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
                         if (mxCryptoError is MXCryptoError.Base &&
                                 mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
                             // need to find sending device
-                            cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-                                val olmContent = event.content.toModel<OlmEventContent>()
-                                cryptoStore.getUserDevices(event.senderId ?: "")
-                                        ?.values
-                                        ?.firstOrNull { it.identityKey() == olmContent?.senderKey }
-                                        ?.let {
-                                            markOlmSessionForUnwedging(event.senderId ?: "", it)
-                                        }
-                                        ?: run {
-                                            Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging")
-                                        }
+                            val olmContent = event.content.toModel<OlmEventContent>()
+                            if (event.senderId != null && olmContent?.senderKey != null) {
+                                markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
+                            } else {
+                                Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
                             }
                         }
                     }
@@ -132,53 +136,91 @@ internal class EventDecryptor @Inject constructor(
         }
     }
 
-    // coroutineDispatchers.crypto scope
-    private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
-        val deviceKey = deviceInfo.identityKey()
+    private fun markOlmSessionForUnwedging(senderId: String, senderKey: String) {
+        val info = WedgedDeviceInfo(senderId, senderKey)
+        if (!wedgedDevices.contains(info)) {
+            Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged")
+            wedgedDevices.add(info)
+        }
+    }
 
-        val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
+    // coroutineDispatchers.crypto scope
+    suspend fun unwedgeDevicesIfNeeded() {
+        // handle wedged devices
+        // Some olm decryption have failed and some device are wedged
+        // we should force start a new session for those
+        Timber.tag(loggerTag.value).v("Unwedging:  ${wedgedDevices.size} are wedged")
+        // get the one that should be retried according to rate limit
         val now = System.currentTimeMillis()
-        if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
-            Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
-            return
+        val toUnwedge = wedgedDevices.filter {
+            val lastForcedDate = lastNewSessionForcedDates[it] ?: 0
+            if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
+                Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate")
+                return@filter false
+            }
+            // let's already mark that we tried now
+            lastNewSessionForcedDates[it] = now
+            true
         }
 
-        Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
-        lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
+        if (toUnwedge.isEmpty()) {
+            Timber.tag(loggerTag.value).v("Nothing to unwedge")
+            return
+        }
+        Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices")
 
-        // offload this from crypto thread (?)
-        cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
-            runCatching { ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) }.fold(
-                    onSuccess = { sendDummyToDevice(ensured = it, deviceInfo, senderId) },
-                    onFailure = {
-                        Timber.e("## CRYPTO | markOlmSessionForUnwedging() : failed to ensure device info ${senderId}${deviceInfo.deviceId}")
+        toUnwedge
+                .chunked(100) // safer to chunk if we ever have lots of wedged devices
+                .forEach { wedgedList ->
+                    val groupedByUserId = wedgedList.groupBy { it.userId }
+                    // lets download keys if needed
+                    withContext(coroutineDispatchers.io) {
+                        deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false)
                     }
-            )
-        }
-    }
 
-    private suspend fun sendDummyToDevice(ensured: MXUsersDevicesMap<MXOlmSessionResult>, deviceInfo: CryptoDeviceInfo, senderId: String) {
-        Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}")
-
-        // Now send a blank message on that session so the other side knows about it.
-        // (The keyshare request is sent in the clear so that won't do)
-        // We send this first such that, as long as the toDevice messages arrive in the
-        // same order we sent them, the other end will get this first, set up the new session,
-        // then get the keyshare request and send the key over this new session (because it
-        // is the session it has most recently received a message on).
-        val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
-
-        val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
-        val sendToDeviceMap = MXUsersDevicesMap<Any>()
-        sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
-        Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}")
-        withContext(coroutineDispatchers.io) {
-            val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
-            try {
-                sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
-            } catch (failure: Throwable) {
-                Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}")
-            }
-        }
+                    // find the matching devices
+                    groupedByUserId
+                            .map { groupedByUser ->
+                                val userId = groupedByUser.key
+                                val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey }
+                                val knownDevices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
+                                userId to wedgeSenderKeysForUser.mapNotNull { senderKey ->
+                                    knownDevices.firstOrNull { it.identityKey() == senderKey }
+                                }
+                            }
+                            .toMap()
+                            .let { deviceList ->
+                                try {
+                                    // force creating new outbound session and mark them as most recent to
+                                    // be used for next encryption (dummy)
+                                    val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true)
+                                    Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to")
+
+                                    // Now send a dummy message on that session so the other side knows about it.
+                                    val payloadJson = mapOf(
+                                            "type" to EventType.DUMMY
+                                    )
+                                    val sendToDeviceMap = MXUsersDevicesMap<Any>()
+                                    sessionToUse.map.values
+                                            .flatMap { it.values }
+                                            .map { it.deviceInfo }
+                                            .forEach { deviceInfo ->
+                                                Timber.tag(loggerTag.value).v("encrypting dummy to ${deviceInfo.deviceId}")
+                                                val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
+                                                sendToDeviceMap.setObject(deviceInfo.userId, deviceInfo.deviceId, encodedPayload)
+                                            }
+
+                                    // now let's send that
+                                    val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
+                                    withContext(coroutineDispatchers.io) {
+                                        sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
+                                    }
+                                } catch (failure: Throwable) {
+                                    deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let {
+                                        Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}")
+                                    }
+                                }
+                            }
+                }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
index e7a46750b0a84afad9c7b07a3a26a37d5a349a22..34bef61c9866d6eb9806770e3490e443d6ac4754 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
@@ -19,8 +19,10 @@ package org.matrix.android.sdk.internal.crypto
 import android.util.LruCache
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import timber.log.Timber
@@ -28,6 +30,13 @@ import java.util.Timer
 import java.util.TimerTask
 import javax.inject.Inject
 
+data class InboundGroupSessionHolder(
+        val wrapper: OlmInboundGroupSessionWrapper2,
+        val mutex: Mutex = Mutex()
+)
+
+private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO)
+
 /**
  * Allows to cache and batch store operations on inbound group session store.
  * Because it is used in the decrypt flow, that can be called quite rapidly
@@ -42,12 +51,13 @@ internal class InboundGroupSessionStore @Inject constructor(
             val senderKey: String
     )
 
-    private val sessionCache = object : LruCache<CacheKey, OlmInboundGroupSessionWrapper2>(30) {
-        override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) {
-            if (evicted && oldValue != null) {
+    private val sessionCache = object : LruCache<CacheKey, InboundGroupSessionHolder>(100) {
+        override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: InboundGroupSessionHolder?, newValue: InboundGroupSessionHolder?) {
+            if (oldValue != null) {
                 cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-                    Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}")
-                    store.storeInboundGroupSessions(listOf(oldValue))
+                    Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
+                    store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
+                    oldValue.wrapper.olmInboundGroupSession?.releaseSession()
                 }
             }
         }
@@ -59,27 +69,50 @@ internal class InboundGroupSessionStore @Inject constructor(
     private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
 
     @Synchronized
-    fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
-        synchronized(sessionCache) {
+    fun clear() {
+        sessionCache.evictAll()
+    }
+
+    @Synchronized
+    fun getInboundGroupSession(sessionId: String, senderKey: String): InboundGroupSessionHolder? {
             val known = sessionCache[CacheKey(sessionId, senderKey)]
-            Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}")
-            return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
-                Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
-                sessionCache.put(CacheKey(sessionId, senderKey), it)
+            Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession  $sessionId in cache ${known != null}")
+            return known
+                    ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
+                Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
+                sessionCache.put(CacheKey(sessionId, senderKey), InboundGroupSessionHolder(it))
+            }?.let {
+                InboundGroupSessionHolder(it)
             }
-        }
     }
 
     @Synchronized
-    fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) {
-        Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}")
+    fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+        Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
+        dirtySession.remove(old.wrapper)
+        store.removeInboundGroupSession(sessionId, senderKey)
+        sessionCache.remove(CacheKey(sessionId, senderKey))
+
+        // release removed session
+        old.wrapper.olmInboundGroupSession?.releaseSession()
+
+        internalStoreGroupSession(new, sessionId, senderKey)
+    }
+
+    @Synchronized
+    fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+        internalStoreGroupSession(holder, sessionId, senderKey)
+    }
+
+    private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+        Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}")
         // We want to batch this a bit for performances
-        dirtySession.add(wrapper)
+        dirtySession.add(holder.wrapper)
 
         if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
             // first time seen, put it in memory cache while waiting for batch insert
             // If it's already known, no need to update cache it's already there
-            sessionCache.put(CacheKey(sessionId, senderKey), wrapper)
+            sessionCache.put(CacheKey(sessionId, senderKey), holder)
         }
 
         timerTask?.cancel()
@@ -96,7 +129,7 @@ internal class InboundGroupSessionStore @Inject constructor(
         val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
         dirtySession.clear()
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-            Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}")
+            Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}")
             tryOrNull {
                 store.storeInboundGroupSessions(toSave)
             }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
index e1a706df799a971f1d161eb5ba78da9408996902..501fb42db2dacf15b93edd198d047a8ae2af74f9 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
@@ -16,6 +16,11 @@
 
 package org.matrix.android.sdk.internal.crypto
 
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
 import org.matrix.android.sdk.api.util.JsonDict
@@ -40,6 +45,8 @@ import timber.log.Timber
 import java.net.URLEncoder
 import javax.inject.Inject
 
+private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO)
+
 // The libolm wrapper.
 @SessionScope
 internal class MXOlmDevice @Inject constructor(
@@ -47,9 +54,12 @@ internal class MXOlmDevice @Inject constructor(
          * The store where crypto data is saved.
          */
         private val store: IMXCryptoStore,
+        private val olmSessionStore: OlmSessionStore,
         private val inboundGroupSessionStore: InboundGroupSessionStore
 ) {
 
+    val mutex = Mutex()
+
     /**
      * @return the Curve25519 key for the account.
      */
@@ -93,26 +103,26 @@ internal class MXOlmDevice @Inject constructor(
         try {
             store.getOrCreateOlmAccount()
         } catch (e: Exception) {
-            Timber.e(e, "MXOlmDevice : cannot initialize olmAccount")
+            Timber.tag(loggerTag.value).e(e, "MXOlmDevice : cannot initialize olmAccount")
         }
 
         try {
             olmUtility = OlmUtility()
         } catch (e: Exception) {
-            Timber.e(e, "## MXOlmDevice : OlmUtility failed with error")
+            Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : OlmUtility failed with error")
             olmUtility = null
         }
 
         try {
-            deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
+            deviceCurve25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] }
         } catch (e: Exception) {
-            Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
+            Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
         }
 
         try {
-            deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
+            deviceEd25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] }
         } catch (e: Exception) {
-            Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
+            Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
         }
     }
 
@@ -121,9 +131,9 @@ internal class MXOlmDevice @Inject constructor(
      */
     fun getOneTimeKeys(): Map<String, Map<String, String>>? {
         try {
-            return store.getOlmAccount().oneTimeKeys()
+            return store.doWithOlmAccount { it.oneTimeKeys() }
         } catch (e: Exception) {
-            Timber.e(e, "## getOneTimeKeys() : failed")
+            Timber.tag(loggerTag.value).e(e, "## getOneTimeKeys() : failed")
         }
 
         return null
@@ -133,7 +143,7 @@ internal class MXOlmDevice @Inject constructor(
      * @return The maximum number of one-time keys the olm account can store.
      */
     fun getMaxNumberOfOneTimeKeys(): Long {
-        return store.getOlmAccount().maxOneTimeKeys()
+        return store.doWithOlmAccount { it.maxOneTimeKeys() }
     }
 
     /**
@@ -143,9 +153,9 @@ internal class MXOlmDevice @Inject constructor(
      */
     fun getFallbackKey(): MutableMap<String, MutableMap<String, String>>? {
         try {
-            return store.getOlmAccount().fallbackKey()
+            return store.doWithOlmAccount { it.fallbackKey() }
         } catch (e: Exception) {
-            Timber.e("## getFallbackKey() : failed")
+            Timber.tag(loggerTag.value).e("## getFallbackKey() : failed")
         }
         return null
     }
@@ -158,12 +168,14 @@ internal class MXOlmDevice @Inject constructor(
     fun generateFallbackKeyIfNeeded(): Boolean {
         try {
             if (!hasUnpublishedFallbackKey()) {
-                store.getOlmAccount().generateFallbackKey()
-                store.saveOlmAccount()
+                store.doWithOlmAccount {
+                    it.generateFallbackKey()
+                    store.saveOlmAccount()
+                }
                 return true
             }
         } catch (e: Exception) {
-            Timber.e("## generateFallbackKey() : failed")
+            Timber.tag(loggerTag.value).e("## generateFallbackKey() : failed")
         }
         return false
     }
@@ -174,10 +186,12 @@ internal class MXOlmDevice @Inject constructor(
 
     fun forgetFallbackKey() {
         try {
-            store.getOlmAccount().forgetFallbackKey()
-            store.saveOlmAccount()
+            store.doWithOlmAccount {
+                it.forgetFallbackKey()
+                store.saveOlmAccount()
+            }
         } catch (e: Exception) {
-            Timber.e("## forgetFallbackKey() : failed")
+            Timber.tag(loggerTag.value).e("## forgetFallbackKey() : failed")
         }
     }
 
@@ -190,6 +204,8 @@ internal class MXOlmDevice @Inject constructor(
             it.groupSession.releaseSession()
         }
         outboundGroupSessionCache.clear()
+        inboundGroupSessionStore.clear()
+        olmSessionStore.clear()
     }
 
     /**
@@ -200,9 +216,9 @@ internal class MXOlmDevice @Inject constructor(
      */
     fun signMessage(message: String): String? {
         try {
-            return store.getOlmAccount().signMessage(message)
+            return store.doWithOlmAccount { it.signMessage(message) }
         } catch (e: Exception) {
-            Timber.e(e, "## signMessage() : failed")
+            Timber.tag(loggerTag.value).e(e, "## signMessage() : failed")
         }
 
         return null
@@ -213,10 +229,12 @@ internal class MXOlmDevice @Inject constructor(
      */
     fun markKeysAsPublished() {
         try {
-            store.getOlmAccount().markOneTimeKeysAsPublished()
-            store.saveOlmAccount()
+            store.doWithOlmAccount {
+                it.markOneTimeKeysAsPublished()
+                store.saveOlmAccount()
+            }
         } catch (e: Exception) {
-            Timber.e(e, "## markKeysAsPublished() : failed")
+            Timber.tag(loggerTag.value).e(e, "## markKeysAsPublished() : failed")
         }
     }
 
@@ -227,10 +245,12 @@ internal class MXOlmDevice @Inject constructor(
      */
     fun generateOneTimeKeys(numKeys: Int) {
         try {
-            store.getOlmAccount().generateOneTimeKeys(numKeys)
-            store.saveOlmAccount()
+            store.doWithOlmAccount {
+                it.generateOneTimeKeys(numKeys)
+                store.saveOlmAccount()
+            }
         } catch (e: Exception) {
-            Timber.e(e, "## generateOneTimeKeys() : failed")
+            Timber.tag(loggerTag.value).e(e, "## generateOneTimeKeys() : failed")
         }
     }
 
@@ -243,12 +263,14 @@ internal class MXOlmDevice @Inject constructor(
      * @return the session id for the outbound session.
      */
     fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? {
-        Timber.v("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey")
+        Timber.tag(loggerTag.value).d("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey")
         var olmSession: OlmSession? = null
 
         try {
             olmSession = OlmSession()
-            olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey)
+            store.doWithOlmAccount { olmAccount ->
+                olmSession.initOutboundSession(olmAccount, theirIdentityKey, theirOneTimeKey)
+            }
 
             val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
 
@@ -257,14 +279,14 @@ internal class MXOlmDevice @Inject constructor(
             // this session
             olmSessionWrapper.onMessageReceived()
 
-            store.storeSession(olmSessionWrapper, theirIdentityKey)
+            olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey)
 
             val sessionIdentifier = olmSession.sessionIdentifier()
 
-            Timber.v("## createOutboundSession() ;  olmSession.sessionIdentifier: $sessionIdentifier")
+            Timber.tag(loggerTag.value).v("## createOutboundSession() ;  olmSession.sessionIdentifier: $sessionIdentifier")
             return sessionIdentifier
         } catch (e: Exception) {
-            Timber.e(e, "## createOutboundSession() failed")
+            Timber.tag(loggerTag.value).e(e, "## createOutboundSession() failed")
 
             olmSession?.releaseSession()
         }
@@ -281,34 +303,38 @@ internal class MXOlmDevice @Inject constructor(
      * @return {{payload: string, session_id: string}} decrypted payload, and session id of new session.
      */
     fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map<String, String>? {
-        Timber.v("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey")
+        Timber.tag(loggerTag.value).d("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey")
 
         var olmSession: OlmSession? = null
 
         try {
             try {
                 olmSession = OlmSession()
-                olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext)
+                store.doWithOlmAccount { olmAccount ->
+                    olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext)
+                }
             } catch (e: Exception) {
-                Timber.e(e, "## createInboundSession() : the session creation failed")
+                Timber.tag(loggerTag.value).e(e, "## createInboundSession() : the session creation failed")
                 return null
             }
 
-            Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
+            Timber.tag(loggerTag.value).v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
 
             try {
-                store.getOlmAccount().removeOneTimeKeys(olmSession)
-                store.saveOlmAccount()
+                store.doWithOlmAccount { olmAccount ->
+                    olmAccount.removeOneTimeKeys(olmSession)
+                    store.saveOlmAccount()
+                }
             } catch (e: Exception) {
-                Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed")
+                Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed")
             }
 
-            Timber.v("## createInboundSession() : ciphertext: $ciphertext")
+            Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext")
             try {
                 val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))
-                Timber.v("## createInboundSession() :ciphertext: SHA256: $sha256")
+                Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256")
             } catch (e: Exception) {
-                Timber.e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
+                Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
             }
 
             val olmMessage = OlmMessage()
@@ -324,9 +350,9 @@ internal class MXOlmDevice @Inject constructor(
                 // This counts as a received message: set last received message time to now
                 olmSessionWrapper.onMessageReceived()
 
-                store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
+                olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
             } catch (e: Exception) {
-                Timber.e(e, "## createInboundSession() : decryptMessage failed")
+                Timber.tag(loggerTag.value).e(e, "## createInboundSession() : decryptMessage failed")
             }
 
             val res = HashMap<String, String>()
@@ -343,7 +369,7 @@ internal class MXOlmDevice @Inject constructor(
 
             return res
         } catch (e: Exception) {
-            Timber.e(e, "## createInboundSession() : OlmSession creation failed")
+            Timber.tag(loggerTag.value).e(e, "## createInboundSession() : OlmSession creation failed")
 
             olmSession?.releaseSession()
         }
@@ -357,8 +383,8 @@ internal class MXOlmDevice @Inject constructor(
      * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
      * @return a list of known session ids for the device.
      */
-    fun getSessionIds(theirDeviceIdentityKey: String): List<String>? {
-        return store.getDeviceSessionIds(theirDeviceIdentityKey)
+    fun getSessionIds(theirDeviceIdentityKey: String): List<String> {
+        return olmSessionStore.getDeviceSessionIds(theirDeviceIdentityKey)
     }
 
     /**
@@ -368,7 +394,7 @@ internal class MXOlmDevice @Inject constructor(
      * @return the session id, or null if no established session.
      */
     fun getSessionId(theirDeviceIdentityKey: String): String? {
-        return store.getLastUsedSessionId(theirDeviceIdentityKey)
+        return olmSessionStore.getLastUsedSessionId(theirDeviceIdentityKey)
     }
 
     /**
@@ -379,30 +405,30 @@ internal class MXOlmDevice @Inject constructor(
      * @param payloadString          the payload to be encrypted and sent
      * @return the cipher text
      */
-    fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map<String, Any>? {
-        var res: MutableMap<String, Any>? = null
-        val olmMessage: OlmMessage
+    suspend fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map<String, Any>? {
         val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
 
         if (olmSessionWrapper != null) {
             try {
-                Timber.v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId")
-                // Timber.v("## encryptMessage() : payloadString: " + payloadString);
-
-                olmMessage = olmSessionWrapper.olmSession.encryptMessage(payloadString)
-                store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
-                res = HashMap()
+                Timber.tag(loggerTag.value).v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId")
 
-                res["body"] = olmMessage.mCipherText
-                res["type"] = olmMessage.mType
-            } catch (e: Exception) {
-                Timber.e(e, "## encryptMessage() : failed")
+                val olmMessage = olmSessionWrapper.mutex.withLock {
+                    olmSessionWrapper.olmSession.encryptMessage(payloadString)
+                }
+                return mapOf(
+                        "body" to olmMessage.mCipherText,
+                        "type" to olmMessage.mType,
+                ).also {
+                    olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
+                }
+            } catch (e: Throwable) {
+                Timber.tag(loggerTag.value).e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId")
+                return null
             }
         } else {
-            Timber.e("## encryptMessage() : Failed to encrypt unknown session $sessionId")
+            Timber.tag(loggerTag.value).e("## encryptMessage() : Failed to encrypt unknown session $sessionId")
+            return null
         }
-
-        return res
     }
 
     /**
@@ -414,7 +440,8 @@ internal class MXOlmDevice @Inject constructor(
      * @param sessionId              the id of the active session.
      * @return the decrypted payload.
      */
-    fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? {
+    @kotlin.jvm.Throws
+    suspend fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? {
         var payloadString: String? = null
 
         val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
@@ -424,13 +451,13 @@ internal class MXOlmDevice @Inject constructor(
             olmMessage.mCipherText = ciphertext
             olmMessage.mType = messageType.toLong()
 
-            try {
-                payloadString = olmSessionWrapper.olmSession.decryptMessage(olmMessage)
-                olmSessionWrapper.onMessageReceived()
-                store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
-            } catch (e: Exception) {
-                Timber.e(e, "## decryptMessage() : decryptMessage failed")
-            }
+            payloadString =
+                    olmSessionWrapper.mutex.withLock {
+                        olmSessionWrapper.olmSession.decryptMessage(olmMessage).also {
+                            olmSessionWrapper.onMessageReceived()
+                        }
+                    }
+            olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
         }
 
         return payloadString
@@ -469,7 +496,7 @@ internal class MXOlmDevice @Inject constructor(
             store.storeCurrentOutboundGroupSessionForRoom(roomId, session)
             return session.sessionIdentifier()
         } catch (e: Exception) {
-            Timber.e(e, "createOutboundGroupSession")
+            Timber.tag(loggerTag.value).e(e, "createOutboundGroupSession")
 
             session?.releaseSession()
         }
@@ -521,7 +548,7 @@ internal class MXOlmDevice @Inject constructor(
             try {
                 return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey()
             } catch (e: Exception) {
-                Timber.e(e, "## getSessionKey() : failed")
+                Timber.tag(loggerTag.value).e(e, "## getSessionKey() : failed")
             }
         }
         return null
@@ -550,8 +577,8 @@ internal class MXOlmDevice @Inject constructor(
         if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
             try {
                 return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString)
-            } catch (e: Exception) {
-                Timber.e(e, "## encryptGroupMessage() : failed")
+            } catch (e: Throwable) {
+                Timber.tag(loggerTag.value).e(e, "## encryptGroupMessage() : failed")
             }
         }
         return null
@@ -578,52 +605,64 @@ internal class MXOlmDevice @Inject constructor(
                                forwardingCurve25519KeyChain: List<String>,
                                keysClaimed: Map<String, String>,
                                exportFormat: Boolean): Boolean {
-        val session = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
-        runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
-                .fold(
-                        {
-                            // If we already have this session, consider updating it
-                            Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
-
-                            val existingFirstKnown = it.firstKnownIndex!!
-                            val newKnownFirstIndex = session.firstKnownIndex
-
-                            // If our existing session is better we keep it
-                            if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
-                                session.olmInboundGroupSession?.releaseSession()
-                                return false
-                            }
-                        },
-                        {
-                            // Nothing to do in case of error
-                        }
-                )
+        val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
+        val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
+        val existingSession = existingSessionHolder?.wrapper
+        // If we have an existing one we should check if the new one is not better
+        if (existingSession != null) {
+            Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
+            try {
+                val existingFirstKnown = existingSession.firstKnownIndex ?: return false.also {
+                    // This is quite unexpected, could throw if native was released?
+                    Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
+                    candidateSession.olmInboundGroupSession?.releaseSession()
+                    // Probably should discard it?
+                }
+                val newKnownFirstIndex = candidateSession.firstKnownIndex
+                // If our existing session is better we keep it
+                if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
+                    Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
+                    candidateSession.olmInboundGroupSession?.releaseSession()
+                    return false
+                }
+            } catch (failure: Throwable) {
+                Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
+                candidateSession.olmInboundGroupSession?.releaseSession()
+                return false
+            }
+        }
 
-        // sanity check
-        if (null == session.olmInboundGroupSession) {
-            Timber.e("## addInboundGroupSession : invalid session")
+        Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
+
+        // sanity check on the new session
+        val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
+        if (null == candidateOlmInboundSession) {
+            Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
             return false
         }
 
         try {
-            if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) {
-                Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
-                session.olmInboundGroupSession!!.releaseSession()
+            if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
+                Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
+                candidateOlmInboundSession.releaseSession()
                 return false
             }
-        } catch (e: Exception) {
-            session.olmInboundGroupSession?.releaseSession()
-            Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed")
+        } catch (e: Throwable) {
+            candidateOlmInboundSession.releaseSession()
+            Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
             return false
         }
 
-        session.senderKey = senderKey
-        session.roomId = roomId
-        session.keysClaimed = keysClaimed
-        session.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
+        candidateSession.senderKey = senderKey
+        candidateSession.roomId = roomId
+        candidateSession.keysClaimed = keysClaimed
+        candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
 
-        inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey)
-//        store.storeInboundGroupSessions(listOf(session))
+        if (existingSession != null) {
+            inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
+        } else {
+            inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
+        }
 
         return true
     }
@@ -638,57 +677,70 @@ internal class MXOlmDevice @Inject constructor(
         val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size)
 
         for (megolmSessionData in megolmSessionsData) {
-            val sessionId = megolmSessionData.sessionId
-            val senderKey = megolmSessionData.senderKey
+            val sessionId = megolmSessionData.sessionId ?: continue
+            val senderKey = megolmSessionData.senderKey ?: continue
             val roomId = megolmSessionData.roomId
 
-            var session: OlmInboundGroupSessionWrapper2? = null
+            var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null
 
             try {
-                session = OlmInboundGroupSessionWrapper2(megolmSessionData)
+                candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData)
             } catch (e: Exception) {
-                Timber.e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
+                Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
             }
 
             // sanity check
-            if (session?.olmInboundGroupSession == null) {
-                Timber.e("## importInboundGroupSession : invalid session")
+            if (candidateSessionToImport?.olmInboundGroupSession == null) {
+                Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session")
                 continue
             }
 
+            val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession
             try {
-                if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) {
-                    Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
-                    if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
+                if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) {
+                    Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
+                    candidateOlmInboundGroupSession?.releaseSession()
                     continue
                 }
             } catch (e: Exception) {
-                Timber.e(e, "## importInboundGroupSession : sessionIdentifier() failed")
-                session.olmInboundGroupSession!!.releaseSession()
+                Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed")
+                candidateOlmInboundGroupSession?.releaseSession()
                 continue
             }
 
-            runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
-                    .fold(
-                            {
-                                // If we already have this session, consider updating it
-                                Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
-
-                                // For now we just ignore updates. TODO: implement something here
-                                if (it.firstKnownIndex!! <= session.firstKnownIndex!!) {
-                                    // Ignore this, keep existing
-                                    session.olmInboundGroupSession!!.releaseSession()
-                                } else {
-                                    sessions.add(session)
-                                }
-                                Unit
-                            },
-                            {
-                                // Session does not already exist, add it
-                                sessions.add(session)
-                            }
-
-                    )
+            val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
+            val existingSession = existingSessionHolder?.wrapper
+
+            if (existingSession == null) {
+                // Session does not already exist, add it
+                Timber.tag(loggerTag.value).d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId")
+                sessions.add(candidateSessionToImport)
+            } else {
+                Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
+                val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex }
+                val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex }
+
+                if (existingFirstKnown == null || candidateFirstKnownIndex == null) {
+                    // should not happen?
+                    candidateSessionToImport.olmInboundGroupSession?.releaseSession()
+                    Timber.tag(loggerTag.value)
+                            .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex")
+                } else {
+                    if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) {
+                        // Ignore this, keep existing
+                        candidateOlmInboundGroupSession.releaseSession()
+                    } else {
+                        // update cache with better session
+                        inboundGroupSessionStore.replaceGroupSession(
+                                existingSessionHolder,
+                                InboundGroupSessionHolder(candidateSessionToImport),
+                                sessionId,
+                                senderKey
+                        )
+                        sessions.add(candidateSessionToImport)
+                    }
+                }
+            }
         }
 
         store.storeInboundGroupSessions(sessions)
@@ -696,18 +748,6 @@ internal class MXOlmDevice @Inject constructor(
         return sessions
     }
 
-    /**
-     * Remove an inbound group session
-     *
-     * @param sessionId  the session identifier.
-     * @param sessionKey base64-encoded secret key.
-     */
-    fun removeInboundGroupSession(sessionId: String?, sessionKey: String?) {
-        if (null != sessionId && null != sessionKey) {
-            store.removeInboundGroupSession(sessionId, sessionKey)
-        }
-    }
-
     /**
      * Decrypt a received message with an inbound group session.
      *
@@ -719,19 +759,24 @@ internal class MXOlmDevice @Inject constructor(
      * @return the decrypting result. Nil if the sessionId is unknown.
      */
     @Throws(MXCryptoError::class)
-    fun decryptGroupMessage(body: String,
-                            roomId: String,
-                            timeline: String?,
-                            sessionId: String,
-                            senderKey: String): OlmDecryptionResult {
-        val session = getInboundGroupSession(sessionId, senderKey, roomId)
+    suspend fun decryptGroupMessage(body: String,
+                                    roomId: String,
+                                    timeline: String?,
+                                    sessionId: String,
+                                    senderKey: String): OlmDecryptionResult {
+        val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
+        val wrapper = sessionHolder.wrapper
+        val inboundGroupSession = wrapper.olmInboundGroupSession
+                ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
         // Check that the room id matches the original one for the session. This stops
         // the HS pretending a message was targeting a different room.
-        if (roomId == session.roomId) {
+        if (roomId == wrapper.roomId) {
             val decryptResult = try {
-                session.olmInboundGroupSession!!.decryptMessage(body)
+                sessionHolder.mutex.withLock {
+                    inboundGroupSession.decryptMessage(body)
+                }
             } catch (e: OlmException) {
-                Timber.e(e, "## decryptGroupMessage () : decryptMessage failed")
+                Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
                 throw MXCryptoError.OlmError(e)
             }
 
@@ -742,32 +787,32 @@ internal class MXOlmDevice @Inject constructor(
 
                 if (timelineSet.contains(messageIndexKey)) {
                     val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
-                    Timber.e("## decryptGroupMessage() : $reason")
+                    Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
                     throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
                 }
 
                 timelineSet.add(messageIndexKey)
             }
 
-            inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey)
+            inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
             val payload = try {
                 val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
                 val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
                 adapter.fromJson(payloadString)
             } catch (e: Exception) {
-                Timber.e("## decryptGroupMessage() : fails to parse the payload")
+                Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload")
                 throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
             }
 
             return OlmDecryptionResult(
                     payload,
-                    session.keysClaimed,
+                    wrapper.keysClaimed,
                     senderKey,
-                    session.forwardingCurve25519KeyChain
+                    wrapper.forwardingCurve25519KeyChain
             )
         } else {
-            val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
-            Timber.e("## decryptGroupMessage() : $reason")
+            val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId)
+            Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
         }
     }
@@ -819,7 +864,7 @@ internal class MXOlmDevice @Inject constructor(
     private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? {
         // sanity check
         return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else {
-            store.getDeviceSession(sessionId, theirDeviceIdentityKey)
+            olmSessionStore.getDeviceSession(sessionId, theirDeviceIdentityKey)
         }
     }
 
@@ -832,25 +877,26 @@ internal class MXOlmDevice @Inject constructor(
      * @param senderKey the base64-encoded curve25519 key of the sender.
      * @return the inbound group session.
      */
-    fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper2 {
+    fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder {
         if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) {
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
         }
 
-        val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
+        val holder = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
+        val session = holder?.wrapper
 
         if (session != null) {
             // Check that the room id matches the original one for the session. This stops
             // the HS pretending a message was targeting a different room.
             if (roomId != session.roomId) {
                 val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
-                Timber.e("## getInboundGroupSession() : $errorDescription")
+                Timber.tag(loggerTag.value).e("## getInboundGroupSession() : $errorDescription")
                 throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)
             } else {
-                return session
+                return holder
             }
         } else {
-            Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
+            Timber.tag(loggerTag.value).w("## getInboundGroupSession() : UISI $sessionId")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
         }
     }
@@ -866,4 +912,9 @@ internal class MXOlmDevice @Inject constructor(
     fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean {
         return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess
     }
+
+    @VisibleForTesting
+    fun clearOlmSessionCache() {
+        olmSessionStore.clear()
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt
index 301729680ce2e91208e8d004f9c5a70caf174e53..9b39a8ab25842bb6fac591833a96cc4faaf91467 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt
@@ -15,6 +15,15 @@
  */
 package org.matrix.android.sdk.internal.crypto
 
+/**
+ * This listener notifies on new Megolm sessions being created
+ */
 interface NewSessionListener {
+
+    /**
+     * @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions
+     * @param senderKey the sender key of the device which the Megolm session is shared with
+     * @param sessionId the session id of the Megolm session
+     */
     fun onNewSession(roomId: String?, senderKey: String, sessionId: String)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f4fbca6a0fe5bc7a29cc5f2f7eacf8d366e0c3a6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.crypto
+
+import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.olm.OlmSession
+import timber.log.Timber
+import javax.inject.Inject
+
+private val loggerTag = LoggerTag("OlmSessionStore", LoggerTag.CRYPTO)
+
+/**
+ * Keep the used olm session in memory and load them from the data layer when needed
+ * Access is synchronized for thread safety
+ */
+internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoStore) {
+    /**
+     * map of device key to list of olm sessions (it is possible to have several active sessions with a device)
+     */
+    private val olmSessions = HashMap<String, MutableList<OlmSessionWrapper>>()
+
+    /**
+     * Store a session between our own device and another device.
+     * This will be called after the session has been created but also every time it has been used
+     * in order to persist the correct state for next run
+     * @param olmSessionWrapper   the end-to-end session.
+     * @param deviceKey the public key of the other device.
+     */
+    @Synchronized
+    fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) {
+        // This could be a newly created session or one that was just created
+        // Anyhow we should persist ratchet state for future app lifecycle
+        addNewSessionInCache(olmSessionWrapper, deviceKey)
+        store.storeSession(olmSessionWrapper, deviceKey)
+    }
+
+    /**
+     * Get all the Olm Sessions we are sharing with the given device.
+     *
+     * @param deviceKey the public key of the other device.
+     * @return A set of sessionId, or empty if device is not known
+     */
+    @Synchronized
+    fun getDeviceSessionIds(deviceKey: String): List<String> {
+        // we need to get the persisted ids first
+        val persistedKnownSessions = store.getDeviceSessionIds(deviceKey)
+                .orEmpty()
+                .toMutableList()
+        // Do we have some in cache not yet persisted?
+        olmSessions.getOrPut(deviceKey) { mutableListOf() }.forEach { cached ->
+            getSafeSessionIdentifier(cached.olmSession)?.let { cachedSessionId ->
+                if (!persistedKnownSessions.contains(cachedSessionId)) {
+                    persistedKnownSessions.add(cachedSessionId)
+                }
+            }
+        }
+        return persistedKnownSessions
+    }
+
+    /**
+     * Retrieve an end-to-end session between our own device and another
+     * device.
+     *
+     * @param sessionId the session Id.
+     * @param deviceKey the public key of the other device.
+     * @return the session wrapper if found
+     */
+    @Synchronized
+    fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
+        // get from cache or load and add to cache
+        return internalGetSession(sessionId, deviceKey)
+    }
+
+    /**
+     * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist
+     *
+     * @param deviceKey the public key of the other device.
+     * @return last used sessionId, or null if not found
+     */
+    @Synchronized
+    fun getLastUsedSessionId(deviceKey: String): String? {
+        // We want to avoid to load in memory old session if possible
+        val lastPersistedUsedSession = store.getLastUsedSessionId(deviceKey)
+        var candidate = lastPersistedUsedSession?.let { internalGetSession(it, deviceKey) }
+        // we should check if we have one in cache with a higher last message received?
+        olmSessions[deviceKey].orEmpty().forEach { inCache ->
+            if (inCache.lastReceivedMessageTs > (candidate?.lastReceivedMessageTs ?: 0L)) {
+                candidate = inCache
+            }
+        }
+
+        return candidate?.olmSession?.sessionIdentifier()
+    }
+
+    /**
+     * Release all sessions and clear cache
+     */
+    @Synchronized
+    fun clear() {
+        olmSessions.entries.onEach { entry ->
+            entry.value.onEach { it.olmSession.releaseSession() }
+        }
+        olmSessions.clear()
+    }
+
+    private fun internalGetSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
+        return getSessionInCache(sessionId, deviceKey)
+                ?: // deserialize from store
+                return store.getDeviceSession(sessionId, deviceKey)?.also {
+                    addNewSessionInCache(it, deviceKey)
+                }
+    }
+
+    private fun getSessionInCache(sessionId: String, deviceKey: String): OlmSessionWrapper? {
+        return olmSessions[deviceKey]?.firstOrNull {
+            getSafeSessionIdentifier(it.olmSession) == sessionId
+        }
+    }
+
+    private fun getSafeSessionIdentifier(session: OlmSession): String? {
+        return try {
+            session.sessionIdentifier()
+        } catch (throwable: Throwable) {
+            Timber.tag(loggerTag.value).w("Failed to load sessionId from loaded olm session")
+            null
+        }
+    }
+
+    private fun addNewSessionInCache(session: OlmSessionWrapper, deviceKey: String) {
+        val sessionId = getSafeSessionIdentifier(session.olmSession) ?: return
+        olmSessions.getOrPut(deviceKey) { mutableListOf() }.let {
+            val existing = it.firstOrNull { getSafeSessionIdentifier(it.olmSession) == sessionId }
+            it.add(session)
+            // remove and release if was there but with different instance
+            if (existing != null && existing.olmSession != session.olmSession) {
+                // mm not sure when this could happen
+                // anyhow we should remove and release the one known
+                it.remove(existing)
+                existing.olmSession.releaseSession()
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
index ab2ed04dfbe5793db1c13780bc2e9d7afd9fdd93..87c176612d7fa61e4cf65e17ae9487efa2791faf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
@@ -16,14 +16,18 @@
 
 package org.matrix.android.sdk.internal.crypto.actions
 
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.internal.crypto.model.MXKey
 import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
 import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
-import org.matrix.android.sdk.internal.crypto.model.toDebugString
 import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
+import org.matrix.android.sdk.internal.session.SessionScope
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -31,90 +35,90 @@ private const val ONE_TIME_KEYS_RETRY_COUNT = 3
 
 private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO)
 
+@SessionScope
 internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
         private val olmDevice: MXOlmDevice,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers,
         private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
 
-    suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>, force: Boolean = false): MXUsersDevicesMap<MXOlmSessionResult> {
-        val devicesWithoutSession = ArrayList<CryptoDeviceInfo>()
+    private val ensureMutex = Mutex()
 
-        val results = MXUsersDevicesMap<MXOlmSessionResult>()
+    /**
+     * We want to synchronize a bit here, because we are iterating to check existing olm session and
+     * also adding some
+     */
+    suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>, force: Boolean = false): MXUsersDevicesMap<MXOlmSessionResult> {
+        ensureMutex.withLock {
+            val results = MXUsersDevicesMap<MXOlmSessionResult>()
+            val deviceList = devicesByUser.flatMap { it.value }
+            Timber.tag(loggerTag.value)
+                    .d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}")
+            val devicesToCreateSessionWith = mutableListOf<CryptoDeviceInfo>()
+            if (force) {
+                // we take all devices and will query otk for them
+                devicesToCreateSessionWith.addAll(deviceList)
+            } else {
+                // only peek devices without active session
+                deviceList.forEach { deviceInfo ->
+                    val deviceId = deviceInfo.deviceId
+                    val userId = deviceInfo.userId
+                    val key = deviceInfo.identityKey() ?: return@forEach Unit.also {
+                        Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key")
+                    }
 
-        for ((userId, deviceList) in devicesByUser) {
-            for (deviceInfo in deviceList) {
-                val deviceId = deviceInfo.deviceId
-                val key = deviceInfo.identityKey()
-                if (key == null) {
-                    Timber.w("## CRYPTO | Ignoring device (${deviceInfo.userId}|$deviceId) without identity key")
-                    continue
+                    // is there a session that as been already used?
+                    val sessionId = olmDevice.getSessionId(key)
+                    if (sessionId.isNullOrEmpty()) {
+                        Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list")
+                        devicesToCreateSessionWith.add(deviceInfo)
+                    } else {
+                        Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)")
+                        val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
+                        results.setObject(userId, deviceId, olmSessionResult)
+                    }
                 }
+            }
 
-                val sessionId = olmDevice.getSessionId(key)
-
-                if (sessionId.isNullOrEmpty() || force) {
-                    Timber.tag(loggerTag.value).d("Found no existing olm session (${deviceInfo.userId}|$deviceId) (force=$force)")
-                    devicesWithoutSession.add(deviceInfo)
-                } else {
-                    Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)")
+            if (devicesToCreateSessionWith.isEmpty()) {
+                // no session to create
+                return results
+            }
+            val usersDevicesToClaim = MXUsersDevicesMap<String>().apply {
+                devicesToCreateSessionWith.forEach {
+                    setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE)
                 }
-
-                val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
-                results.setObject(userId, deviceId, olmSessionResult)
             }
-        }
-
-        Timber.tag(loggerTag.value).d("Devices without olm session (count:${devicesWithoutSession.size}) :" +
-                " ${devicesWithoutSession.joinToString { "${it.userId}|${it.deviceId}" }}")
-        if (devicesWithoutSession.size == 0) {
-            return results
-        }
-
-        // Prepare the request for claiming one-time keys
-        val usersDevicesToClaim = MXUsersDevicesMap<String>()
-
-        val oneTimeKeyAlgorithm = MXKey.KEY_SIGNED_CURVE_25519_TYPE
 
-        for (device in devicesWithoutSession) {
-            usersDevicesToClaim.setObject(device.userId, device.deviceId, oneTimeKeyAlgorithm)
-        }
+            // Let's now claim one time keys
+            val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
+            val oneTimeKeys = withContext(coroutineDispatchers.io) {
+                oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT)
+            }
 
-        // TODO: this has a race condition - if we try to send another message
-        // while we are claiming a key, we will end up claiming two and setting up
-        // two sessions.
-        //
-        // That should eventually resolve itself, but it's poor form.
-
-        Timber.tag(loggerTag.value).i("claimOneTimeKeysForUsersDevices() : ${usersDevicesToClaim.toDebugString()}")
-
-        val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
-        val oneTimeKeys = oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, remainingRetry = ONE_TIME_KEYS_RETRY_COUNT)
-        Timber.tag(loggerTag.value).v("claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
-        for ((userId, deviceInfos) in devicesByUser) {
-            for (deviceInfo in deviceInfos) {
-                var oneTimeKey: MXKey? = null
-                val deviceIds = oneTimeKeys.getUserDeviceIds(userId)
-                if (null != deviceIds) {
-                    for (deviceId in deviceIds) {
-                        val olmSessionResult = results.getObject(userId, deviceId)
-                        if (olmSessionResult?.sessionId != null && !force) {
-                            // We already have a result for this device
-                            continue
-                        }
-                        val key = oneTimeKeys.getObject(userId, deviceId)
-                        if (key?.type == oneTimeKeyAlgorithm) {
-                            oneTimeKey = key
-                        }
-                        if (oneTimeKey == null) {
-                            Timber.tag(loggerTag.value).d("No one time key for $userId|$deviceId")
-                            continue
-                        }
-                        // Update the result for this device in results
-                        olmSessionResult?.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
+            // let now start olm session using the new otks
+            devicesToCreateSessionWith.forEach { deviceInfo ->
+                val userId = deviceInfo.userId
+                val deviceId = deviceInfo.deviceId
+                // Did we get an OTK
+                val oneTimeKey = oneTimeKeys.getObject(userId, deviceId)
+                if (oneTimeKey == null) {
+                    Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}")
+                } else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) {
+                    Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}")
+                } else {
+                    val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
+                    if (olmSessionId != null) {
+                        val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId)
+                        results.setObject(userId, deviceId, olmSessionResult)
+                    } else {
+                        Timber
+                                .tag(loggerTag.value)
+                                .d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}")
                     }
                 }
             }
+            return results
         }
-        return results
     }
 
     private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt
index 0d78f68e5c6527f5c5b4e17ebce369859560d171..f79b97b0818a9d73f38bcb63f892cdea6917292b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt
@@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.MegolmSessionData
 import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
 import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider
+import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
 import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
 import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@@ -76,7 +77,11 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
                     outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
 
                     // Have another go at decrypting events sent with this session
-                    decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!)
+                    when (decrypting) {
+                        is MXMegolmDecryption -> {
+                            decrypting.onNewSession(megolmSessionData.roomId, megolmSessionData.senderKey!!, sessionId!!)
+                        }
+                    }
                 } catch (e: Exception) {
                     Timber.e(e, "## importRoomKeys() : onNewSession failed")
                 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt
index 165f200bacfab78d40c09c7914bc6ae1914a85db..4e158602c8957a5d275a70093e2d9b88584afa2f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.actions
 
+import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_OLM
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
@@ -28,6 +29,8 @@ import org.matrix.android.sdk.internal.util.convertToUTF8
 import timber.log.Timber
 import javax.inject.Inject
 
+private val loggerTag = LoggerTag("MessageEncrypter", LoggerTag.CRYPTO)
+
 internal class MessageEncrypter @Inject constructor(
         @UserId
         private val userId: String,
@@ -42,7 +45,7 @@ internal class MessageEncrypter @Inject constructor(
      * @param deviceInfos   list of device infos to encrypt for.
      * @return the content for an m.room.encrypted event.
      */
-    fun encryptMessage(payloadFields: Content, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage {
+    suspend fun encryptMessage(payloadFields: Content, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage {
         val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
 
         val payloadJson = payloadFields.toMutableMap()
@@ -66,7 +69,7 @@ internal class MessageEncrypter @Inject constructor(
             val sessionId = olmDevice.getSessionId(deviceKey)
 
             if (!sessionId.isNullOrEmpty()) {
-                Timber.v("Using sessionid $sessionId for device $deviceKey")
+                Timber.tag(loggerTag.value).d("Using sessionid $sessionId for device $deviceKey")
 
                 payloadJson["recipient"] = deviceInfo.userId
                 payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
index 79c7608cbf1ba9e6820f86bcb1dadf64b8cda8c5..51ddd7444226bf7bccb576f34e8e9a25213ef1f7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
@@ -36,7 +36,7 @@ internal interface IMXDecrypting {
      * @return the decryption information, or an error
      */
     @Throws(MXCryptoError::class)
-    fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
+    suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
 
     /**
      * Handle a key event.
@@ -45,14 +45,6 @@ internal interface IMXDecrypting {
      */
     fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
 
-    /**
-     * Check if the some messages can be decrypted with a new session
-     *
-     * @param senderKey the session sender key
-     * @param sessionId the session id
-     */
-    fun onNewSession(senderKey: String, sessionId: String) {}
-
     /**
      * Determine if we have the keys necessary to respond to a room key request
      *
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt
index 1fd5061a65c8e1b99c8aca3c064d153c4275a029..6f488def0af0d366df361e76999efcfa1e0f637f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt
@@ -45,7 +45,7 @@ internal interface IMXGroupEncryption {
      *
      * @return true in case of success
      */
-    suspend fun reshareKey(sessionId: String,
+    suspend fun reshareKey(groupSessionId: String,
                            userId: String,
                            deviceId: String,
                            senderKey: String): Boolean
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
index 2ee24dfbb064d29cfa93a99778fd6e88801e0ee6..72df59023a394cca812b9b36b5c221e90cb2b79e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 import dagger.Lazy
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.withLock
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@@ -71,7 +72,7 @@ internal class MXMegolmDecryption(private val userId: String,
 //    private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
 
     @Throws(MXCryptoError::class)
-    override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         // If cross signing is enabled, we don't send request until the keys are trusted
         // There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once
         val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true
@@ -79,7 +80,7 @@ internal class MXMegolmDecryption(private val userId: String,
     }
 
     @Throws(MXCryptoError::class)
-    private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
+    private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
         Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail")
         if (event.roomId.isNullOrBlank()) {
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
@@ -317,19 +318,20 @@ internal class MXMegolmDecryption(private val userId: String,
 
             outgoingGossipingRequestManager.cancelRoomKeyRequest(content)
 
-            onNewSession(senderKey, roomKeyContent.sessionId)
+            onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId)
         }
     }
 
     /**
      * Check if the some messages can be decrypted with a new session
      *
+     * @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions
      * @param senderKey the session sender key
      * @param sessionId the session id
      */
-    override fun onNewSession(senderKey: String, sessionId: String) {
+    fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
         Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
-        newSessionListener?.onNewSession(null, senderKey, sessionId)
+        newSessionListener?.onNewSession(roomId, senderKey, sessionId)
     }
 
     override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean {
@@ -345,7 +347,22 @@ internal class MXMegolmDecryption(private val userId: String,
             return
         }
         val userId = request.userId ?: return
+
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+            val body = request.requestBody
+            val sessionHolder = try {
+                olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId)
+            } catch (failure: Throwable) {
+                Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session for request $body")
+                return@launch
+            }
+
+            val export = sessionHolder.mutex.withLock {
+                sessionHolder.wrapper.exportKeys()
+            } ?: return@launch Unit.also {
+                Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${body.sessionId}")
+            }
+
             runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
                     .mapCatching {
                         val deviceId = request.deviceId
@@ -355,7 +372,6 @@ internal class MXMegolmDecryption(private val userId: String,
                         } else {
                             val devicesByUser = mapOf(userId to listOf(deviceInfo))
                             val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
-                            val body = request.requestBody
                             val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
                             if (olmSessionResult?.sessionId == null) {
                                 // no session with this device, probably because there
@@ -365,19 +381,10 @@ internal class MXMegolmDecryption(private val userId: String,
                             }
                             Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId")
 
-                            val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
-                            runCatching { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) }
-                                    .fold(
-                                            {
-                                                // TODO
-                                                payloadJson["content"] = it.exportKeys() ?: ""
-                                            },
-                                            {
-                                                // TODO
-                                                Timber.tag(loggerTag.value).e(it, "shareKeysWithDevice: failed to get session for request $body")
-                                            }
-
-                                    )
+                            val payloadJson = mapOf(
+                                    "type" to EventType.FORWARDED_ROOM_KEY,
+                                    "content" to export
+                            )
 
                             val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
                             val sendToDeviceMap = MXUsersDevicesMap<Any>()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
index 389036a1f86bd964907d50774e71bac5937afa44..cf9733dc2dc155fe3f4ba397e007f0f067f36b5d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@@ -88,7 +90,7 @@ internal class MXMegolmEncryption(
         Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom")
         val devices = getDevicesInRoom(userIds)
         Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}")
-        Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}")
+        Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}")
         val outboundSession = ensureOutboundSession(devices.allowedDevices)
 
         return encryptContent(outboundSession, eventType, eventContent)
@@ -142,8 +144,9 @@ internal class MXMegolmEncryption(
         Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ")
         val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
 
-        val keysClaimedMap = HashMap<String, String>()
-        keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
+        val keysClaimedMap = mapOf(
+                "ed25519" to olmDevice.deviceEd25519Key!!
+        )
 
         olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
                 emptyList(), keysClaimedMap, false)
@@ -303,11 +306,13 @@ internal class MXMegolmEncryption(
             Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}")
             val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
             try {
-                sendToDeviceTask.execute(sendToDeviceParams)
+                withContext(coroutineDispatchers.io) {
+                    sendToDeviceTask.execute(sendToDeviceParams)
+                }
                 Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
             } catch (failure: Throwable) {
                 // What to do here...
-                Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ")
+                Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>")
             }
         } else {
             Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
@@ -346,9 +351,12 @@ internal class MXMegolmEncryption(
                 }
         )
         try {
-            sendToDeviceTask.execute(params)
+            withContext(coroutineDispatchers.io) {
+                sendToDeviceTask.execute(params)
+            }
         } catch (failure: Throwable) {
-            Timber.tag(loggerTag.value).e("notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
+            Timber.tag(loggerTag.value)
+                    .e("notifyKeyWithHeld() :$sessionId Failed to send withheld  ${targets.map { "${it.userId}|${it.deviceId}" }}")
         }
     }
 
@@ -432,20 +440,20 @@ internal class MXMegolmEncryption(
         }
     }
 
-    override suspend fun reshareKey(sessionId: String,
+    override suspend fun reshareKey(groupSessionId: String,
                                     userId: String,
                                     deviceId: String,
                                     senderKey: String): Boolean {
-        Timber.tag(loggerTag.value).i("process reshareKey for $sessionId to $userId:$deviceId")
+        Timber.tag(loggerTag.value).i("process reshareKey for $groupSessionId to $userId:$deviceId")
         val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false
                 .also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") }
 
         // Get the chain index of the key we previously sent this device
-        val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, sessionId, deviceInfo)
+        val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, groupSessionId, deviceInfo)
         if (!wasSessionSharedWithUser.found) {
             // This session was never shared with this user
             // Send a room key with held
-            notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
+            notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), groupSessionId, senderKey, WithHeldCode.UNAUTHORISED)
             Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device")
             return false
         }
@@ -456,42 +464,47 @@ internal class MXMegolmEncryption(
                 }
 
         val devicesByUser = mapOf(userId to listOf(deviceInfo))
-        val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
-        val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
-        olmSessionResult?.sessionId // no session with this device, probably because there were no one-time keys.
-                // ensureOlmSessionsForDevicesAction has already done the logging, so just skip it.
-                ?: return false.also {
-                    Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys")
-                }
-
-        Timber.tag(loggerTag.value).i(" reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId")
+        val usersDeviceMap = try {
+            ensureOlmSessionsForDevicesAction.handle(devicesByUser)
+        } catch (failure: Throwable) {
+            null
+        }
+        val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId)
+        if (olmSessionResult?.sessionId == null) {
+            Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys")
+            return false
+        }
+        Timber.tag(loggerTag.value).i(" reshareKey: $groupSessionId:$chainIndex with device $userId:$deviceId using session ${olmSessionResult.sessionId}")
 
-        val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
+        val sessionHolder = try {
+            olmDevice.getInboundGroupSession(groupSessionId, senderKey, roomId)
+        } catch (failure: Throwable) {
+            Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session $groupSessionId")
+            return false
+        }
 
-        runCatching { olmDevice.getInboundGroupSession(sessionId, senderKey, roomId) }
-                .fold(
-                        {
-                            // TODO
-                            payloadJson["content"] = it.exportKeys(chainIndex.toLong()) ?: ""
-                        },
-                        {
-                            // TODO
-                            Timber.tag(loggerTag.value).e(it, "reshareKey: failed to get session $sessionId|$senderKey|$roomId")
-                        }
+        val export = sessionHolder.mutex.withLock {
+            sessionHolder.wrapper.exportKeys()
+        } ?: return false.also {
+            Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session $groupSessionId")
+        }
 
-                )
+        val payloadJson = mapOf(
+                "type" to EventType.FORWARDED_ROOM_KEY,
+                "content" to export
+        )
 
         val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
         val sendToDeviceMap = MXUsersDevicesMap<Any>()
         sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
-        Timber.tag(loggerTag.value).i("reshareKey() : sending session $sessionId to $userId:$deviceId")
+        Timber.tag(loggerTag.value).i("reshareKey() : sending session $groupSessionId to $userId:$deviceId")
         val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
         return try {
             sendToDeviceTask.execute(sendToDeviceParams)
-            Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$sessionId> to $userId:$deviceId")
+            Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$groupSessionId> to $userId:$deviceId")
             true
         } catch (failure: Throwable) {
-            Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
+            Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$groupSessionId> to $userId:$deviceId")
             false
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt
index f1bca4fbc6b181c0f6902ea8835bfd7aa3370e4a..afa249801d90c1bc398aa146d2d86873767303be 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt
@@ -16,6 +16,8 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms.olm
 
+import kotlinx.coroutines.sync.withLock
+import org.matrix.android.sdk.api.logger.LoggerTag
 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.toModel
@@ -30,6 +32,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.util.convertFromUTF8
 import timber.log.Timber
 
+private val loggerTag = LoggerTag("MXOlmDecryption", LoggerTag.CRYPTO)
 internal class MXOlmDecryption(
         // The olm device interface
         private val olmDevice: MXOlmDevice,
@@ -38,27 +41,27 @@ internal class MXOlmDecryption(
     IMXDecrypting {
 
     @Throws(MXCryptoError::class)
-    override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
-            Timber.e("## decryptEvent() : bad event format")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
                     MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON)
         }
 
         val cipherText = olmEventContent.ciphertext ?: run {
-            Timber.e("## decryptEvent() : missing cipher text")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
                     MXCryptoError.MISSING_CIPHER_TEXT_REASON)
         }
 
         val senderKey = olmEventContent.senderKey ?: run {
-            Timber.e("## decryptEvent() : missing sender key")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY,
                     MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON)
         }
 
         val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
-            Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)
         }
 
@@ -69,7 +72,7 @@ internal class MXOlmDecryption(
         val decryptedPayload = decryptMessage(message, senderKey)
 
         if (decryptedPayload == null) {
-            Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
+            Timber.tag(loggerTag.value).e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
         }
         val payloadString = convertFromUTF8(decryptedPayload)
@@ -78,30 +81,30 @@ internal class MXOlmDecryption(
         val payload = adapter.fromJson(payloadString)
 
         if (payload == null) {
-            Timber.e("## decryptEvent failed : null payload")
+            Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)
         }
 
         val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run {
-            Timber.e("## decryptEvent() : bad olmPayloadContent format")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
         }
 
         if (olmPayloadContent.recipient.isNullOrBlank()) {
             val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
-            Timber.e("## decryptEvent() : $reason")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : $reason")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
         }
 
         if (olmPayloadContent.recipient != userId) {
-            Timber.e("## decryptEvent() : Event ${event.eventId}:" +
+            Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}:" +
                     " Intended recipient ${olmPayloadContent.recipient} does not match our id $userId")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT,
                     String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient))
         }
 
         val recipientKeys = olmPayloadContent.recipientKeys ?: run {
-            Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
+            Timber.tag(loggerTag.value).e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
                     " property; cannot prevent unknown-key attack")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
                     String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys"))
@@ -110,31 +113,34 @@ internal class MXOlmDecryption(
         val ed25519 = recipientKeys["ed25519"]
 
         if (ed25519 != olmDevice.deviceEd25519Key) {
-            Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
                     MXCryptoError.BAD_RECIPIENT_KEY_REASON)
         }
 
         if (olmPayloadContent.sender.isNullOrBlank()) {
-            Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
+            Timber.tag(loggerTag.value)
+                    .e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
                     String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender"))
         }
 
         if (olmPayloadContent.sender != event.senderId) {
-            Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
+            Timber.tag(loggerTag.value)
+                    .e("Event ${event.eventId}:  sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE,
                     String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender))
         }
 
         if (olmPayloadContent.roomId != event.roomId) {
-            Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
+            Timber.tag(loggerTag.value)
+                    .e("## decryptEvent() : Event ${event.eventId}:  room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM,
                     String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId))
         }
 
         val keys = olmPayloadContent.keys ?: run {
-            Timber.e("## decryptEvent failed : null keys")
+            Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
                     MXCryptoError.MISSING_CIPHER_TEXT_REASON)
         }
@@ -153,8 +159,8 @@ internal class MXOlmDecryption(
      * @param message                message object, with 'type' and 'body' fields.
      * @return payload, if decrypted successfully.
      */
-    private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
-        val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey).orEmpty()
+    private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
+        val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey)
 
         val messageBody = message["body"] as? String ?: return null
         val messageType = when (val typeAsVoid = message["type"]) {
@@ -166,11 +172,32 @@ internal class MXOlmDecryption(
 
         // Try each session in turn
         // decryptionErrors = {};
+
+        val isPreKey = messageType == 0
+        // we want to synchronize on prekey if not we could end up create two olm sessions
+        // Not very clear but it looks like the js-sdk for consistency
+        return if (isPreKey) {
+            olmDevice.mutex.withLock {
+                reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
+            }
+        } else {
+            reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
+        }
+    }
+
+    private suspend fun reallyDecryptMessage(sessionIds: List<String>, messageBody: String, messageType: Int, theirDeviceIdentityKey: String): String? {
+        Timber.tag(loggerTag.value).d("decryptMessage() try to decrypt olm message type:$messageType from ${sessionIds.size} known sessions")
         for (sessionId in sessionIds) {
-            val payload = olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
+            val payload = try {
+                olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
+            } catch (throwable: Exception) {
+                // As we are trying one by one, we don't really care of the error here
+                Timber.tag(loggerTag.value).d("decryptMessage() failed with session $sessionId")
+                null
+            }
 
             if (null != payload) {
-                Timber.v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
+                Timber.tag(loggerTag.value).v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
                 return payload
             } else {
                 val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody)
@@ -178,7 +205,7 @@ internal class MXOlmDecryption(
                 if (foundSession) {
                     // Decryption failed, but it was a prekey message matching this
                     // session, so it should have worked.
-                    Timber.e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO")
+                    Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO")
                     return null
                 }
             }
@@ -189,9 +216,9 @@ internal class MXOlmDecryption(
             // didn't work.
 
             if (sessionIds.isEmpty()) {
-                Timber.e("## decryptMessage() : No existing sessions")
+                Timber.tag(loggerTag.value).e("## decryptMessage() : No existing sessions")
             } else {
-                Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
+                Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
             }
 
             return null
@@ -199,14 +226,17 @@ internal class MXOlmDecryption(
 
         // prekey message which doesn't match any existing sessions: make a new
         // session.
+        // XXXX Possible races here? if concurrent access for same prekey message, we might create 2 sessions?
+        Timber.tag(loggerTag.value).d("## decryptMessage() :  Create inbound group session from prekey sender:$theirDeviceIdentityKey")
+
         val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody)
 
         if (null == res) {
-            Timber.e("## decryptMessage() :  Error decrypting non-prekey message with existing sessions")
+            Timber.tag(loggerTag.value).e("## decryptMessage() :  Error decrypting non-prekey message with existing sessions")
             return null
         }
 
-        Timber.v("## decryptMessage() :  Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
+        Timber.tag(loggerTag.value).v("## decryptMessage() :  Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
 
         return res["payload"]
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
index 5cd647ff6fa05058ca1189f4638c32e0a1686341..794ab0453321bf30a86ab247c81bbf3ecb034304 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
@@ -52,7 +52,7 @@ import timber.log.Timber
 import javax.inject.Inject
 
 internal class UpdateTrustWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
-    SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, sessionManager, Params::class.java) {
+        SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, sessionManager, Params::class.java) {
 
     @JsonClass(generateAdapter = true)
     internal data class Params(
@@ -96,7 +96,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
         if (userList.isNotEmpty()) {
             // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
             // or a new device?) So we check all again :/
-            Timber.d("## CrossSigning - Updating trust for users: ${userList.logLimit()}")
+            Timber.v("## CrossSigning - Updating trust for users: ${userList.logLimit()}")
             updateTrust(userList)
         }
 
@@ -148,7 +148,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                     myUserId -> myTrustResult
                     else     -> {
                         crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
-                            Timber.d("## CrossSigning - user:${entry.key} result:$it")
+                            Timber.v("## CrossSigning - user:${entry.key} result:$it")
                         }
                     }
                 }
@@ -178,7 +178,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                 // Update trust if needed
                 devicesEntities?.forEach { device ->
                     val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
-                    Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
+                    Timber.v("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
                     if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
                         Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
                         // need to save
@@ -216,7 +216,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                                     .equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
                                     .findFirst()
                                     ?.let { roomSummary ->
-                                        Timber.d("## CrossSigning - Check shield state for room $roomId")
+                                        Timber.v("## CrossSigning - Check shield state for room $roomId")
                                         val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
                                         try {
                                             val updatedTrust = computeRoomShield(
@@ -277,7 +277,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                                   cryptoRealm: Realm,
                                   activeMemberUserIds: List<String>,
                                   roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
-        Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
+        Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
         // The set of “all users” depends on the type of room:
         // For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room
         // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
index b20168eaa3cd25ffe869d8ce252ae3595ee8f142..954c2dbe43185fa4a0b3b0c035e03bf5d24ec018 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
@@ -671,7 +671,6 @@ internal class DefaultKeysBackupService @Inject constructor(
                         Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
                         throw InvalidParameterException("Invalid recovery key")
                     }
-
                     // Get a PK decryption instance
                     pkDecryptionFromRecoveryKey(recoveryKey)
                 }
@@ -681,6 +680,10 @@ internal class DefaultKeysBackupService @Inject constructor(
                     throw InvalidParameterException("Invalid recovery key")
                 }
 
+                // Save for next time and for gossiping
+                // Save now as it's valid, don't wait for the import as it could take long.
+                saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
+
                 stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
 
                 // Get backed up keys from the homeserver
@@ -729,8 +732,6 @@ internal class DefaultKeysBackupService @Inject constructor(
                     if (backUp) {
                         maybeBackupKeys()
                     }
-                    // Save for next time and for gossiping
-                    saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
                     result
                 }
             }.foldToCallback(callback)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
index 5e7744853aa464a9515fb1fb9c807e1c5321617d..b3638dc414d60976d7054ff2e421856d8f0c37ea 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
@@ -70,6 +70,8 @@ data class CryptoDeviceInfo(
         keys?.let { map["keys"] = it }
         return map
     }
+
+    fun shortDebugString() = "$userId|$deviceId"
 }
 
 internal fun CryptoDeviceInfo.toRest(): DeviceKeys {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt
index 662541428e91bedaa19fee4ffee382a682a20314..bdb00dce8e608ee2855650974fe1315a7a3e4982 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt
@@ -130,7 +130,7 @@ inline fun <T> MXUsersDevicesMap<T>.forEach(action: (String, String, T) -> Unit)
     }
 }
 
-internal  fun <T> MXUsersDevicesMap<T>.toDebugString() =
+internal fun <T> MXUsersDevicesMap<T>.toDebugString() =
         map.entries.joinToString { "${it.key} [${it.value.keys.joinToString { it }}]" }
 
 internal fun <T> MXUsersDevicesMap<T>.toDebugCount() =
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt
index 15b92f105ae63d2ccac18b885ee11f198eb89e25..263cb3b0362e82fa0bde0ea11dd0d714fb77fee4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.model
 
+import kotlinx.coroutines.sync.Mutex
 import org.matrix.olm.OlmSession
 
 /**
@@ -25,7 +26,10 @@ data class OlmSessionWrapper(
         // The associated olm session.
         val olmSession: OlmSession,
         // Timestamp at which the session last received a message.
-        var lastReceivedMessageTs: Long = 0) {
+        var lastReceivedMessageTs: Long = 0,
+
+        val mutex: Mutex = Mutex()
+) {
 
     /**
      * Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs`
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 96ea5c03fa2540533d33558d08d6e1dafdf55853..e662ff74e72fd48ae50917c35e5193eb60f4d4c3 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
@@ -54,7 +54,7 @@ internal interface IMXCryptoStore {
     /**
      * @return the olm account
      */
-    fun getOlmAccount(): OlmAccount
+    fun <T> doWithOlmAccount(block: (OlmAccount) -> T): T
 
     fun getOrCreateOlmAccount(): OlmAccount
 
@@ -261,7 +261,7 @@ internal interface IMXCryptoStore {
     fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String)
 
     /**
-     * Retrieve the end-to-end session ids between the logged-in user and another
+     * Retrieve all end-to-end session ids between our own device and another
      * device.
      *
      * @param deviceKey the public key of the other device.
@@ -270,7 +270,7 @@ internal interface IMXCryptoStore {
     fun getDeviceSessionIds(deviceKey: String): List<String>?
 
     /**
-     * Retrieve an end-to-end session between the logged-in user and another
+     * Retrieve an end-to-end session between our own device and another
      * device.
      *
      * @param sessionId the session Id.
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 a07827c033a94c7c58b927600ca47ef1d45cfa7d..585b3d2d25c2ed01676809ac03c7b0ca09b35a1f 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
@@ -104,7 +104,6 @@ import timber.log.Timber
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
-import kotlin.collections.set
 
 @SessionScope
 internal class RealmCryptoStore @Inject constructor(
@@ -124,12 +123,6 @@ internal class RealmCryptoStore @Inject constructor(
     // The olm account
     private var olmAccount: OlmAccount? = null
 
-    // Cache for OlmSession, to release them properly
-    private val olmSessionsToRelease = HashMap<String, OlmSessionWrapper>()
-
-    // Cache for InboundGroupSession, to release them properly
-    private val inboundGroupSessionToRelease = HashMap<String, OlmInboundGroupSessionWrapper2>()
-
     private val newSessionListeners = ArrayList<NewSessionListener>()
 
     override fun addNewSessionListener(listener: NewSessionListener) {
@@ -213,16 +206,6 @@ internal class RealmCryptoStore @Inject constructor(
             monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES)
         }
 
-        olmSessionsToRelease.forEach {
-            it.value.olmSession.releaseSession()
-        }
-        olmSessionsToRelease.clear()
-
-        inboundGroupSessionToRelease.forEach {
-            it.value.olmInboundGroupSession?.releaseSession()
-        }
-        inboundGroupSessionToRelease.clear()
-
         olmAccount?.releaseAccount()
 
         realmLocker?.close()
@@ -247,10 +230,18 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
-    override fun getOlmAccount(): OlmAccount {
-        return olmAccount!!
+    /**
+     * Olm account access should be synchronized
+     */
+    override fun <T> doWithOlmAccount(block: (OlmAccount) -> T): T {
+        return olmAccount!!.let { olmAccount ->
+            synchronized(olmAccount) {
+                block.invoke(olmAccount)
+            }
+        }
     }
 
+    @Synchronized
     override fun getOrCreateOlmAccount(): OlmAccount {
         doRealmTransaction(realmConfiguration) {
             val metaData = it.where<CryptoMetadataEntity>().findFirst()
@@ -680,13 +671,6 @@ internal class RealmCryptoStore @Inject constructor(
         if (sessionIdentifier != null) {
             val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey)
 
-            // Release memory of previously known session, if it is not the same one
-            if (olmSessionsToRelease[key]?.olmSession != olmSessionWrapper.olmSession) {
-                olmSessionsToRelease[key]?.olmSession?.releaseSession()
-            }
-
-            olmSessionsToRelease[key] = olmSessionWrapper
-
             doRealmTransaction(realmConfiguration) {
                 val realmOlmSession = OlmSessionEntity().apply {
                     primaryKey = key
@@ -703,23 +687,18 @@ internal class RealmCryptoStore @Inject constructor(
 
     override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
         val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey)
-
-        // If not in cache (or not found), try to read it from realm
-        if (olmSessionsToRelease[key] == null) {
-            doRealmQueryAndCopy(realmConfiguration) {
-                it.where<OlmSessionEntity>()
-                        .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key)
-                        .findFirst()
-            }
-                    ?.let {
-                        val olmSession = it.getOlmSession()
-                        if (olmSession != null && it.sessionId != null) {
-                            olmSessionsToRelease[key] = OlmSessionWrapper(olmSession, it.lastReceivedMessageTs)
-                        }
-                    }
+        return doRealmQueryAndCopy(realmConfiguration) {
+            it.where<OlmSessionEntity>()
+                    .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key)
+                    .findFirst()
         }
-
-        return olmSessionsToRelease[key]
+                ?.let {
+                    val olmSession = it.getOlmSession()
+                    if (olmSession != null && it.sessionId != null) {
+                        return@let OlmSessionWrapper(olmSession, it.lastReceivedMessageTs)
+                    }
+                    null
+                }
     }
 
     override fun getLastUsedSessionId(deviceKey: String): String? {
@@ -761,13 +740,6 @@ internal class RealmCryptoStore @Inject constructor(
                 if (sessionIdentifier != null) {
                     val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
 
-                    // Release memory of previously known session, if it is not the same one
-                    if (inboundGroupSessionToRelease[key] != session) {
-                        inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession()
-                    }
-
-                    inboundGroupSessionToRelease[key] = session
-
                     val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
                         primaryKey = key
                         sessionId = sessionIdentifier
@@ -784,20 +756,12 @@ internal class RealmCryptoStore @Inject constructor(
     override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
         val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
 
-        // If not in cache (or not found), try to read it from realm
-        if (inboundGroupSessionToRelease[key] == null) {
-            doWithRealm(realmConfiguration) {
-                it.where<OlmInboundGroupSessionEntity>()
-                        .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
-                        .findFirst()
-                        ?.getInboundGroupSession()
-            }
-                    ?.let {
-                        inboundGroupSessionToRelease[key] = it
-                    }
+        return doWithRealm(realmConfiguration) {
+            it.where<OlmInboundGroupSessionEntity>()
+                    .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
+                    .findFirst()
+                    ?.getInboundGroupSession()
         }
-
-        return inboundGroupSessionToRelease[key]
     }
 
     override fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? {
@@ -853,10 +817,6 @@ internal class RealmCryptoStore @Inject constructor(
     override fun removeInboundGroupSession(sessionId: String, senderKey: String) {
         val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
 
-        // Release memory of previously known session
-        inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession()
-        inboundGroupSessionToRelease.remove(key)
-
         doRealmTransaction(realmConfiguration) {
             it.where<OlmInboundGroupSessionEntity>()
                     .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt
index 6839ccd3262c5679c27d3e977196a0405f0e1752..04ce0d85002c67c83489fe184286115c1f760938 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt
@@ -70,7 +70,7 @@ object HkdfSha256 {
            T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
            T(3) = HMAC-Hash(PRK, T(2) | info | 0x03)
            ...
-        */
+         */
         val n = ceil(outputLength.toDouble() / HASH_LEN.toDouble()).toInt()
 
         var stepHash = ByteArray(0) // T(0) empty string (zero length)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
index 388ecb965952802695fff2d543fa7b8347599a9d..bd623575fafdbfcdfe4821c082189e8076488825 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
@@ -364,14 +364,14 @@ internal class DefaultVerificationService @Inject constructor(
         dispatchRequestAdded(pendingVerificationRequest)
 
         /*
-        * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event
-        * to begin the verification.
-        * If both parties send an m.key.verification.start event, and they both specify the same verification method,
-        * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start
-        * event is ignored.
-        * In the case of a single user verifying two of their devices, the device ID is compared instead.
-        * If both parties send an m.key.verification.start event, but they specify different verification methods,
-        * the verification should be cancelled with a code of m.unexpected_message.
+         * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event
+         * to begin the verification.
+         * If both parties send an m.key.verification.start event, and they both specify the same verification method,
+         * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start
+         * event is ignored.
+         * In the case of a single user verifying two of their devices, the device ID is compared instead.
+         * If both parties send an m.key.verification.start event, but they specify different verification methods,
+         * the verification should be cancelled with a code of m.unexpected_message.
          */
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
index 829e066bf3924ef1a8f40d06ff0cf5867fa536ec..90ede18dc8b1969143b6d674c3d3810ef00431cd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
@@ -29,7 +29,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationTransaction
 import org.matrix.android.sdk.internal.crypto.verification.ValidVerificationInfoStart
-import org.matrix.android.sdk.internal.util.exhaustive
 import timber.log.Timber
 
 internal class DefaultQrCodeVerificationTransaction(
@@ -129,7 +128,7 @@ internal class DefaultQrCodeVerificationTransaction(
                     // Nothing special here, we will send a reciprocate start event, and then the other session will trust it's view of the MSK
                 }
             }
-        }.exhaustive
+        }
 
         val toVerifyDeviceIds = mutableListOf<String>()
 
@@ -174,7 +173,7 @@ internal class DefaultQrCodeVerificationTransaction(
                     Unit
                 }
             }
-        }.exhaustive
+        }
 
         if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
             // Nothing to verify
@@ -272,6 +271,7 @@ internal class DefaultQrCodeVerificationTransaction(
                 // I now know that i can trust my MSK
                 trust(true, emptyList(), true)
             }
+            null                                           -> Unit
         }
     }
 
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 12e60da114541268264c3c84b74c8eafa26b24ce..a57397dad5f10595c3c4fa72d1e32ced108f73e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo022
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026
 import org.matrix.android.sdk.internal.util.Normalizer
 import timber.log.Timber
 import javax.inject.Inject
@@ -57,7 +58,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
     override fun equals(other: Any?) = other is RealmSessionStoreMigration
     override fun hashCode() = 1000
 
-    val schemaVersion = 25L
+    val schemaVersion = 26L
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
         Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
@@ -87,5 +88,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 23) MigrateSessionTo023(realm).perform()
         if (oldVersion < 24) MigrateSessionTo024(realm).perform()
         if (oldVersion < 25) MigrateSessionTo025(realm).perform()
+        if (oldVersion < 26) MigrateSessionTo026(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
index 289db9fa15b4351adc99ae2df221d3fd619cbd60..d2e3e99b755a7e52a63ed661e6c19a1438c3adc5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
@@ -82,17 +82,18 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
 internal fun ChunkEntity.addTimelineEvent(roomId: String,
                                           eventEntity: EventEntity,
                                           direction: PaginationDirection,
-                                          roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null) {
+                                          ownedByThreadChunk: Boolean = false,
+                                          roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null): TimelineEventEntity? {
     val eventId = eventEntity.eventId
     if (timelineEvents.find(eventId) != null) {
-        return
+        return null
     }
     val displayIndex = nextDisplayIndex(direction)
     val localId = TimelineEventEntity.nextId(realm)
     val senderId = eventEntity.sender ?: ""
 
     // Update RR for the sender of a new message with a dummy one
-    val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId)
+    val readReceiptsSummaryEntity = if (!ownedByThreadChunk) handleReadReceipts(realm, roomId, eventEntity, senderId) else null
     val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
         this.localId = localId
         this.root = eventEntity
@@ -102,6 +103,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
                 ?.also { it.cleanUp(eventEntity.sender) }
         this.readReceipts = readReceiptsSummaryEntity
         this.displayIndex = displayIndex
+        this.ownedByThreadChunk = ownedByThreadChunk
         val roomMemberContent = roomMemberContentsByUser?.get(senderId)
         this.senderAvatar = roomMemberContent?.avatarUrl
         this.senderName = roomMemberContent?.displayName
@@ -113,9 +115,10 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
     }
     // numberOfTimelineEvents++
     timelineEvents.add(timelineEventEntity)
+    return timelineEventEntity
 }
 
-private fun computeIsUnique(
+fun computeIsUnique(
         realm: Realm,
         roomId: String,
         isLastForward: Boolean,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
index 724f307e3bda3648bf01b53cb11e5dd7c4f7bb80..9ad2708b4381ae456234042efb6c4c4b9b373b0f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
@@ -18,9 +18,16 @@ package org.matrix.android.sdk.internal.database.helper
 
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 
 internal fun RoomEntity.addIfNecessary(chunkEntity: ChunkEntity) {
     if (!chunks.contains(chunkEntity)) {
         chunks.add(chunkEntity)
     }
 }
+
+internal fun RoomEntity.addIfNecessary(threadSummary: ThreadSummaryEntity) {
+    if (!threadSummaries.contains(threadSummary)) {
+        threadSummaries.add(threadSummary)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index f703bfaf82adcac49a4c96697ae90b099ac7267d..04cf5b78af79e8dc4d3994782ac11768d6a55580 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -16,9 +16,12 @@
 
 package org.matrix.android.sdk.internal.database.helper
 
+import com.squareup.moshi.JsonDataException
 import io.realm.Realm
 import io.realm.RealmQuery
 import io.realm.Sort
+import org.matrix.android.sdk.api.session.events.model.UnsignedData
+import org.matrix.android.sdk.api.session.events.model.isRedacted
 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
@@ -33,8 +36,10 @@ 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
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import timber.log.Timber
 
-private typealias ThreadSummary = Pair<Int, TimelineEventEntity>?
+private typealias Summary = Pair<Int, TimelineEventEntity>?
 
 /**
  * Finds the root thread event and update it with the latest message summary along with the number
@@ -48,14 +53,14 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
     for ((rootThreadEventId, eventEntity) in this) {
         eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId, chunkEntity)?.let { threadSummary ->
 
-            val numberOfMessages = threadSummary.first
+            val inThreadMessages = 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,
+                    inThreadMessages = inThreadMessages,
                     latestMessageTimelineEventEntity = latestEventInThread
             )
         }
@@ -81,27 +86,27 @@ internal fun EventEntity.findRootThreadEvent(): EventEntity? =
  * Mark or update the current event a root thread event
  */
 internal fun EventEntity.markEventAsRoot(
-        threadsCounted: Int,
+        inThreadMessages: Int,
         latestMessageTimelineEventEntity: TimelineEventEntity?) {
     isRootThread = true
-    numberOfThreads = threadsCounted
+    numberOfThreads = inThreadMessages
     threadSummaryLatestMessage = latestMessageTimelineEventEntity
 }
 
 /**
  * Count the number of threads for the provided root thread eventId, and finds the latest event message
+ * note: Redactions are handled by RedactionEventProcessor
  * @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()
+internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary {
+    val inThreadMessages = countInThreadMessages(
+            realm = realm,
+            roomId = roomId,
+            rootThreadEventId = rootThreadEventId
+    )
 
-    if (messages <= 0) return null
+    if (inThreadMessages <= 0) return null
 
     // Find latest thread event, we know it exists
     var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: chunkEntity ?: return null
@@ -123,9 +128,38 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId:
 
     result ?: return null
 
-    return ThreadSummary(messages, result)
+    return Summary(inThreadMessages, result)
 }
 
+/**
+ * Counts the number of thread replies in the main timeline thread summary,
+ * with respect to redactions.
+ */
+internal fun countInThreadMessages(realm: Realm, roomId: String, rootThreadEventId: String): Int =
+        TimelineEventEntity
+                .whereRoomId(realm, roomId = roomId)
+                .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+                .distinct(TimelineEventEntityFields.ROOT.EVENT_ID)
+                .findAll()
+                .filterNot { timelineEvent ->
+                    timelineEvent.root
+                            ?.unsignedData
+                            ?.takeIf { it.isNotBlank() }
+                            ?.toUnsignedData()
+                            .isRedacted()
+                }.size
+
+/**
+ * Mapping string to UnsignedData using Moshi
+ */
+private fun String.toUnsignedData(): UnsignedData? =
+        try {
+            MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(this)
+        } catch (ex: JsonDataException) {
+            Timber.e(ex, "Failed to parse UnsignedData")
+            null
+        }
+
 /**
  * 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
@@ -156,6 +190,7 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm,
         TimelineEventEntity
                 .whereRoomId(realm, roomId = roomId)
                 .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
+                .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
                 .sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING)
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7087f071621d2b8cc90aa99fffad2365d948d849
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.helper
+
+import io.realm.Realm
+import io.realm.RealmQuery
+import io.realm.Sort
+import io.realm.kotlin.createObject
+import org.matrix.android.sdk.api.session.crypto.CryptoService
+import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
+import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
+import org.matrix.android.sdk.internal.database.mapper.asDomain
+import org.matrix.android.sdk.internal.database.mapper.toEntity
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.EventInsertType
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
+import org.matrix.android.sdk.internal.database.query.getOrCreate
+import org.matrix.android.sdk.internal.database.query.getOrNull
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
+import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
+import timber.log.Timber
+import java.util.UUID
+
+internal fun ThreadSummaryEntity.updateThreadSummary(
+        rootThreadEventEntity: EventEntity,
+        numberOfThreads: Int?,
+        latestThreadEventEntity: EventEntity?,
+        isUserParticipating: Boolean,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>) {
+    updateThreadSummaryRootEvent(rootThreadEventEntity, roomMemberContentsByUser)
+    updateThreadSummaryLatestEvent(latestThreadEventEntity, roomMemberContentsByUser)
+    this.isUserParticipating = isUserParticipating
+    numberOfThreads?.let {
+        // Update number of threads only when there is an actual value
+        this.numberOfThreads = it
+    }
+}
+
+/**
+ * Updates the root thread event properties
+ */
+internal fun ThreadSummaryEntity.updateThreadSummaryRootEvent(
+        rootThreadEventEntity: EventEntity,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>
+) {
+    val roomId = rootThreadEventEntity.roomId
+    val rootThreadRoomMemberContent = roomMemberContentsByUser[rootThreadEventEntity.sender ?: ""]
+    this.rootThreadEventEntity = rootThreadEventEntity
+    this.rootThreadSenderAvatar = rootThreadRoomMemberContent?.avatarUrl
+    this.rootThreadSenderName = rootThreadRoomMemberContent?.displayName
+    this.rootThreadIsUniqueDisplayName = if (rootThreadRoomMemberContent?.displayName != null) {
+        computeIsUnique(realm, roomId, false, rootThreadRoomMemberContent, roomMemberContentsByUser)
+    } else {
+        true
+    }
+}
+
+/**
+ * Updates the latest thread event properties
+ */
+internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent(
+        latestThreadEventEntity: EventEntity?,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>
+) {
+    val roomId = latestThreadEventEntity?.roomId ?: return
+    val latestThreadRoomMemberContent = roomMemberContentsByUser[latestThreadEventEntity.sender ?: ""]
+    this.latestThreadEventEntity = latestThreadEventEntity
+    this.latestThreadSenderAvatar = latestThreadRoomMemberContent?.avatarUrl
+    this.latestThreadSenderName = latestThreadRoomMemberContent?.displayName
+    this.latestThreadIsUniqueDisplayName = if (latestThreadRoomMemberContent?.displayName != null) {
+        computeIsUnique(realm, roomId, false, latestThreadRoomMemberContent, roomMemberContentsByUser)
+    } else {
+        true
+    }
+}
+
+private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap<String, RoomMemberContent?>): TimelineEventEntity {
+    val roomId = roomId
+    val eventId = eventId
+    val localId = TimelineEventEntity.nextId(realm)
+    val senderId = sender ?: ""
+
+    val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
+        this.localId = localId
+        this.root = this@toTimelineEventEntity
+        this.eventId = eventId
+        this.roomId = roomId
+        this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
+                ?.also { it.cleanUp(sender) }
+        this.ownedByThreadChunk = true  // To skip it from the original event flow
+        val roomMemberContent = roomMemberContentsByUser[senderId]
+        this.senderAvatar = roomMemberContent?.avatarUrl
+        this.senderName = roomMemberContent?.displayName
+        isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
+            computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser)
+        } else {
+            true
+        }
+    }
+    return timelineEventEntity
+}
+
+internal suspend fun ThreadSummaryEntity.Companion.createOrUpdate(
+        threadSummaryType: ThreadSummaryUpdateType,
+        realm: Realm,
+        roomId: String,
+        threadEventEntity: EventEntity? = null,
+        rootThreadEvent: Event? = null,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>,
+        roomEntity: RoomEntity,
+        userId: String,
+        cryptoService: CryptoService? = null
+) {
+    when (threadSummaryType) {
+        ThreadSummaryUpdateType.REPLACE -> {
+            rootThreadEvent?.eventId ?: return
+            rootThreadEvent.senderId ?: return
+
+            val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return
+
+            // Something is wrong with the server return
+            if (numberOfThreads <= 0) return
+
+            val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also {
+                Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ")
+            }
+
+            val rootThreadEventEntity = createEventEntity(roomId, rootThreadEvent, realm).also {
+                decryptIfNeeded(cryptoService, it, roomId)
+            }
+            val latestThreadEventEntity = createLatestEventEntity(roomId, rootThreadEvent, roomMemberContentsByUser, realm)?.also {
+                decryptIfNeeded(cryptoService, it, roomId)
+            }
+            val isUserParticipating = rootThreadEvent.unsignedData.relations.latestThread.isUserParticipating == true || rootThreadEvent.senderId == userId
+            roomMemberContentsByUser.addSenderState(realm, roomId, rootThreadEvent.senderId)
+            threadSummary.updateThreadSummary(
+                    rootThreadEventEntity = rootThreadEventEntity,
+                    numberOfThreads = numberOfThreads,
+                    latestThreadEventEntity = latestThreadEventEntity,
+                    isUserParticipating = isUserParticipating,
+                    roomMemberContentsByUser = roomMemberContentsByUser
+            )
+
+            roomEntity.addIfNecessary(threadSummary)
+        }
+        ThreadSummaryUpdateType.ADD     -> {
+            val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return
+            Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId")
+
+            val threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId)
+            if (threadSummary != null) {
+                // ThreadSummary exists so lets add the latest event
+                Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.")
+                threadSummary.updateThreadSummaryLatestEvent(threadEventEntity, roomMemberContentsByUser)
+                threadSummary.numberOfThreads++
+                if (threadEventEntity.sender == userId) {
+                    threadSummary.isUserParticipating = true
+                }
+            } else {
+                // ThreadSummary do not exists lets try to create one
+                Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one")
+                threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity ->
+                    // Root thread event entity exists so lets create a new record
+                    ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let {
+                        it.updateThreadSummary(
+                                rootThreadEventEntity = rootThreadEventEntity,
+                                numberOfThreads = 1,
+                                latestThreadEventEntity = threadEventEntity,
+                                isUserParticipating = threadEventEntity.sender == userId,
+                                roomMemberContentsByUser = roomMemberContentsByUser
+                        )
+                        roomEntity.addIfNecessary(it)
+                    }
+                }
+            }
+        }
+    }
+}
+
+private suspend fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEntity, roomId: String) {
+    cryptoService ?: return
+    val event = eventEntity.asDomain()
+    if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) {
+        try {
+            Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
+            // Event from sync does not have roomId, so add it to the event first
+            val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
+            event.mxDecryptionResult = OlmDecryptionResult(
+                    payload = result.clearEvent,
+                    senderKey = result.senderCurve25519Key,
+                    keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
+                    forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
+            )
+            // Save decryption result, to not decrypt every time we enter the thread list
+            eventEntity.setDecryptionResult(result)
+        } catch (e: MXCryptoError) {
+            if (e is MXCryptoError.Base) {
+                event.mCryptoError = e.errorType
+                event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
+            }
+        }
+    }
+}
+
+/**
+ * Request decryption
+ */
+private fun requestDecryption(eventDecryptor: TimelineEventDecryptor?, event: Event?) {
+    eventDecryptor ?: return
+    event ?: return
+    if (event.isEncrypted() &&
+            event.mxDecryptionResult == null && event.eventId != null) {
+        Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
+
+        eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(event, UUID.randomUUID().toString()))
+    }
+}
+
+/**
+ * If we don't have any new state on this user, get it from db
+ */
+private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roomId: String, senderId: String) {
+    getOrPut(senderId) {
+        CurrentStateEventEntity
+                .getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)
+                ?.root?.asDomain()
+                ?.getFixedRoomMemberContent()
+    }
+}
+
+/**
+ * Create an EventEntity for the root thread event or get an existing one
+ */
+private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
+    val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+    return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+}
+
+/**
+ * Create an EventEntity for the latest thread event or get an existing one. Also update the user room member
+ * state
+ */
+private fun createLatestEventEntity(
+        roomId: String,
+        rootThreadEvent: Event,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>,
+        realm: Realm): EventEntity? {
+    return getLatestEvent(rootThreadEvent)?.let {
+        it.senderId?.let { senderId ->
+            roomMemberContentsByUser.addSenderState(realm, roomId, senderId)
+        }
+        createEventEntity(roomId, it, realm)
+    }
+}
+
+/**
+ * Returned the latest event message, if any
+ */
+private fun getLatestEvent(rootThreadEvent: Event): Event? {
+    return rootThreadEvent.unsignedData?.relations?.latestThread?.event
+}
+
+/**
+ * Find all ThreadSummaryEntity for the specified roomId, sorted by origin server
+ * note: Sorting cannot be provided by server, so we have to use that unstable property
+ * @param roomId The id of the room
+ */
+internal fun ThreadSummaryEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> =
+        ThreadSummaryEntity
+                .where(realm, roomId = roomId)
+                .sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING)
+
+/**
+ * Enhance each [ThreadSummary] root and latest event with the equivalent decrypted text edition/replacement
+ */
+internal fun List<ThreadSummary>.enhanceWithEditions(realm: Realm, roomId: String): List<ThreadSummary> =
+        this.map {
+            it.addEditionIfNeeded(realm, roomId, true)
+            it.addEditionIfNeeded(realm, roomId, false)
+            it
+        }
+
+private fun ThreadSummary.addEditionIfNeeded(realm: Realm, roomId: String, enhanceRoot: Boolean) {
+    val eventId = if (enhanceRoot) rootEventId else latestEvent?.eventId ?: return
+    EventAnnotationsSummaryEntity
+            .where(realm, roomId, eventId)
+            .findFirst()
+            ?.editSummary
+            ?.editions
+            ?.lastOrNull()
+            ?.eventId
+            ?.let { editedEventId ->
+                TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent ->
+                    if (enhanceRoot) {
+                        threadEditions.rootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
+                    } else {
+                        threadEditions.latestThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
+                    }
+                }
+            }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt
index 700b94a98582b1600e25b9b18d7678099c50c7a2..069e539e2c04505ace0b66743c8cfd12d9069de9 100644
--- 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
@@ -19,15 +19,19 @@ package org.matrix.android.sdk.internal.database.lightweight
 import android.content.Context
 import androidx.core.content.edit
 import androidx.preference.PreferenceManager
+import org.matrix.android.sdk.api.MatrixConfiguration
 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
+ * on the sdk 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) {
+class LightweightSettingsStorage  @Inject constructor(
+        context: Context,
+        private val matrixConfiguration: MatrixConfiguration
+) {
 
     private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
 
@@ -38,7 +42,7 @@ class LightweightSettingsStorage  @Inject constructor(context: Context) {
     }
 
     fun areThreadMessagesEnabled(): Boolean {
-        return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, false)
+        return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, matrixConfiguration.threadMessagesEnabledDefault)
     }
 
     companion object {
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 9c420e81fd70759fdaf70876d43e79fe755daddd..c3302f5ccbe9b52d4fcc844b209d3c6ad4732d63 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
@@ -114,7 +114,7 @@ internal object EventMapper {
                         )
                     },
                     threadNotificationState = eventEntity.threadNotificationState,
-                    threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary(),
+                    threadSummaryLatestEvent = eventEntity.threadSummaryLatestMessage?.root?.asDomain(),
                     lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs
 
             )
diff --git a/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 7869506015e282845a4dab84bd17ed19f6a1fd2c..2e33988a22b853f941add399e3da3464928e2417 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
@@ -41,7 +41,8 @@ internal object HomeServerCapabilitiesMapper {
                 maxUploadFileSize = entity.maxUploadFileSize,
                 lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
                 defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
-                roomVersions = mapRoomVersion(entity.roomVersionsJson)
+                roomVersions = mapRoomVersion(entity.roomVersionsJson),
+                canUseThreading = entity.canUseThreading
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cedb9e3d452a409dad32fffe8e14d9a9209c35f2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.mapper
+
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import javax.inject.Inject
+
+internal class ThreadSummaryMapper @Inject constructor() {
+
+    fun map(threadSummary: ThreadSummaryEntity): ThreadSummary {
+        return ThreadSummary(
+                roomId = threadSummary.room?.firstOrNull()?.roomId.orEmpty(),
+                rootEvent = threadSummary.rootThreadEventEntity?.asDomain(),
+                latestEvent = threadSummary.latestThreadEventEntity?.asDomain(),
+                rootEventId = threadSummary.rootThreadEventId.orEmpty(),
+                rootThreadSenderInfo = SenderInfo(
+                        userId = threadSummary.rootThreadEventEntity?.sender ?: "",
+                        displayName = threadSummary.rootThreadSenderName,
+                        isUniqueDisplayName = threadSummary.rootThreadIsUniqueDisplayName,
+                        avatarUrl = threadSummary.rootThreadSenderAvatar
+                ),
+                latestThreadSenderInfo = SenderInfo(
+                        userId = threadSummary.latestThreadEventEntity?.sender ?: "",
+                        displayName = threadSummary.latestThreadSenderName,
+                        isUniqueDisplayName = threadSummary.latestThreadIsUniqueDisplayName,
+                        avatarUrl = threadSummary.latestThreadSenderAvatar
+                ),
+                isUserParticipating = threadSummary.isUserParticipating,
+                numberOfThreads = threadSummary.numberOfThreads
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
index 55c7f2a8ee605a7283e54bf0d66622275ae3e4c4..e6586224444cd7de1c5aa938d13e8d7059910bc1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
@@ -46,6 +46,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
                         isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
                         avatarUrl = timelineEventEntity.senderAvatar
                 ),
+                ownedByThreadChunk = timelineEventEntity.ownedByThreadChunk,
                 readReceipts = readReceipts
                         ?.distinctBy {
                             it.roomMember
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f108a91ecf01c6f36a656cb3fc54e753bddbac38
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.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.internal.database.model.ChunkEntityFields
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomEntityFields
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+/**
+ * Migrating to:
+ * Live thread list: using enhanced /messages api MSC3440
+ * Live thread timeline: using /relations api
+ */
+class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("ChunkEntity")
+                ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+                ?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
+
+        realm.schema.get("TimelineEventEntity")
+                ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java)
+
+        val eventEntity = realm.schema.get("EventEntity") ?: return
+        val threadSummaryEntity = realm.schema.create("ThreadSummaryEntity")
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java)
+                .addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java)
+                .addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java)
+                .addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity)
+                .addRealmObjectField(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.`$`, eventEntity)
+
+        realm.schema.get("RoomEntity")
+                ?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity)
+
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java)
+                ?.forceRefreshOfHomeServerCapabilities()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
index c45c27ed08a0fe2f361ae17d3ccf64b1d4384ac0..88eb821aa9d9088a2241a30d0309cc30a3983dcf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
@@ -33,7 +33,10 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
                                 var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
         // Only one chunk will have isLastForward == true
                                 @Index var isLastForward: Boolean = false,
-                                @Index var isLastBackward: Boolean = false
+                                @Index var isLastBackward: Boolean = false,
+        // Threads
+                                @Index var rootThreadEventId: String? = null,
+                                @Index var isLastForwardThread: Boolean = false,
 ) : RealmObject() {
 
     fun identifier() = "${prevToken}_$nextToken"
@@ -47,14 +50,32 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
     companion object
 }
 
-internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) {
+internal fun ChunkEntity.deleteOnCascade(
+        deleteStateEvents: Boolean,
+        canDeleteRoot: Boolean) {
     assertIsManaged()
     if (deleteStateEvents) {
         stateEvents.deleteAllFromRealm()
     }
     timelineEvents.clearWith {
         val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents)
+        if (deleteRoot) {
+            room?.firstOrNull()?.removeThreadSummaryIfNeeded(it.eventId)
+        }
         it.deleteOnCascade(deleteRoot)
     }
     deleteFromRealm()
 }
+
+/**
+ * Delete the chunk along with the thread events that were temporarily created
+ */
+internal fun ChunkEntity.deleteAndClearThreadEvents() {
+    assertIsManaged()
+    timelineEvents
+            .filter { it.ownedByThreadChunk }
+            .forEach {
+                it.deleteOnCascade(false)
+            }
+    deleteFromRealm()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
index 445181e576416c50a287701c98cf6d2efb8bf1c9..09be98aa9609d53653bdfd995a27c2359134b62a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
@@ -34,16 +34,17 @@ internal open class EventEntity(@Index var eventId: String = "",
                                 @Index var stateKey: String? = null,
                                 var originServerTs: Long? = null,
                                 @Index var sender: String? = null,
-                                // Can contain a serialized MatrixError
+        // Can contain a serialized MatrixError
                                 var sendStateDetails: String? = null,
                                 var age: Long? = 0,
                                 var unsignedData: String? = null,
                                 var redacts: String? = null,
                                 var decryptionResultJson: String? = null,
                                 var ageLocalTs: Long? = null,
-                                // Thread related, no need to create a new Entity for performance
+        // Thread related, no need to create a new Entity for performance
                                 @Index var isRootThread: Boolean = false,
                                 @Index var rootThreadEventId: String? = null,
+        // Number messages within the thread
                                 var numberOfThreads: Int = 0,
                                 var threadSummaryLatestMessage: TimelineEventEntity? = null
 ) : RealmObject() {
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 08ecd5995ec490c8a14cfc35f1186b35740e2f9e..47a83f0ed998fbbd27713416a302b90bde103809 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
@@ -28,7 +28,8 @@ internal open class HomeServerCapabilitiesEntity(
         var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
         var lastVersionIdentityServerSupported: Boolean = false,
         var defaultIdentityServerUrl: String? = null,
-        var lastUpdatedTimestamp: Long = 0L
+        var lastUpdatedTimestamp: Long = 0L,
+        var canUseThreading: Boolean = false
 ) : RealmObject() {
 
     companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
index 2997d5d7d8ad4a4a1e3f1f17cb3fbefdbd50c6b3..4a6f6a7bf8970a893e6f018431db61ee57a0a7ee 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
@@ -20,10 +20,14 @@ import io.realm.RealmList
 import io.realm.RealmObject
 import io.realm.annotations.PrimaryKey
 import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.query.findRootOrLatest
+import org.matrix.android.sdk.internal.extensions.assertIsManaged
 
 internal open class RoomEntity(@PrimaryKey var roomId: String = "",
                                var chunks: RealmList<ChunkEntity> = RealmList(),
                                var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
+                               var threadSummaries: RealmList<ThreadSummaryEntity> = RealmList(),
                                var accountData: RealmList<RoomAccountDataEntity> = RealmList()
 ) : RealmObject() {
 
@@ -46,3 +50,10 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
         }
     companion object
 }
+internal fun RoomEntity.removeThreadSummaryIfNeeded(eventId: String) {
+    assertIsManaged()
+    threadSummaries.findRootOrLatest(eventId)?.let {
+        threadSummaries.remove(it)
+        it.deleteFromRealm()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index c090777972194dc6cef0f40f286bb796f6f7d1a4..d0d23dd491b3336676cd4561a184cf8d5655c17e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.model
 
 import io.realm.annotations.RealmModule
 import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 
 /**
  * Realm module for Session
@@ -66,6 +67,7 @@ import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntit
             RoomAccountDataEntity::class,
             SpaceChildSummaryEntity::class,
             SpaceParentSummaryEntity::class,
-            UserPresenceEntity::class
+            UserPresenceEntity::class,
+            ThreadSummaryEntity::class
         ])
 internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
index 185f0e2dcc4bd52ff202879a0c37729c884bee8b..aacd6570bc4768ed37c7ea26e292b3f3e3051cef 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
@@ -32,6 +32,9 @@ internal open class TimelineEventEntity(var localId: Long = 0,
                                         var isUniqueDisplayName: Boolean = false,
                                         var senderAvatar: String? = null,
                                         var senderMembershipEventId: String? = null,
+                                        // ownedByThreadChunk indicates that the current TimelineEventEntity belongs
+                                        // to a thread chunk and is a temporarily event.
+                                        var ownedByThreadChunk: Boolean = false,
                                         var readReceipts: ReadReceiptsSummaryEntity? = null
 ) : RealmObject() {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ab9d66548ed418a47652a8f0f4944b084869c8aa
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.model.threads
+
+import io.realm.RealmObject
+import io.realm.RealmResults
+import io.realm.annotations.Index
+import io.realm.annotations.LinkingObjects
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+
+internal open class ThreadSummaryEntity(@Index var rootThreadEventId: String? = "",
+                                        var rootThreadEventEntity: EventEntity? = null,
+                                        var latestThreadEventEntity: EventEntity? = null,
+                                        var rootThreadSenderName: String? = null,
+                                        var latestThreadSenderName: String? = null,
+                                        var rootThreadSenderAvatar: String? = null,
+                                        var latestThreadSenderAvatar: String? = null,
+                                        var rootThreadIsUniqueDisplayName: Boolean = false,
+                                        var isUserParticipating: Boolean = false,
+                                        var latestThreadIsUniqueDisplayName: Boolean = false,
+                                        var numberOfThreads: Int = 0
+) : RealmObject() {
+
+    @LinkingObjects("threadSummaries")
+    val room: RealmResults<RoomEntity>? = null
+
+    companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
index 156a8dd767c9acd266d171a9f51365da28ea7cf4..ece46555a7e145af189912cf9a76a551aee23774 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
@@ -45,10 +45,22 @@ internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, room
             .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
             .findFirst()
 }
-
+internal fun ChunkEntity.Companion.findLastForwardChunkOfThread(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity? {
+    return where(realm, roomId)
+            .equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+            .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
+            .findFirst()
+}
+internal fun ChunkEntity.Companion.findEventInThreadChunk(realm: Realm, roomId: String, event: String): ChunkEntity? {
+    return where(realm, roomId)
+            .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, arrayListOf(event).toTypedArray())
+            .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
+            .findFirst()
+}
 internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List<String>): RealmResults<ChunkEntity> {
     return realm.where<ChunkEntity>()
             .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray())
+            .isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
             .findAll()
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
index 14cb7e22da2dabc9ee2132a8efdb7ea4c4e25078..6caa832110ccee65587a7420a8b5f97f796b0d7c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
@@ -34,7 +34,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId
         this.roomId = roomId
     }
     // Denormalization
-    TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let {
+    TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findAll()?.forEach {
         it.annotations = obj
     }
     return obj
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt
index d1b05a4932c2dc70e7028dec264a82fe292b230f..8993c36a300f1d41b7c510b2eb06d3e315ca9f1e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt
@@ -49,6 +49,10 @@ internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: Strin
     return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
 }
 
+internal fun RoomSummaryEntity.Companion.getOrNull(realm: Realm, roomId: String): RoomSummaryEntity? {
+    return where(realm, roomId).findFirst()
+}
+
 internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm,
                                                         excludeRoomIds: Set<String>? = null): RealmResults<RoomSummaryEntity> {
     return RoomSummaryEntity.where(realm)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt
new file mode 100644
index 0000000000000000000000000000000000000000..517d43d7cf965efe4593be5144d3028e45a2ddfe
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.query
+
+import io.realm.Realm
+import io.realm.RealmList
+import io.realm.RealmQuery
+import io.realm.kotlin.createObject
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+
+internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> {
+    return realm.where<ThreadSummaryEntity>()
+            .equalTo(ThreadSummaryEntityFields.ROOM.ROOM_ID, roomId)
+}
+
+internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String, rootThreadEventId: String): RealmQuery<ThreadSummaryEntity> {
+    return where(realm, roomId)
+            .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+}
+
+internal fun ThreadSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity {
+    return where(realm, roomId, rootThreadEventId).findFirst() ?: realm.createObject<ThreadSummaryEntity>().apply {
+        this.rootThreadEventId = rootThreadEventId
+    }
+}
+internal fun ThreadSummaryEntity.Companion.getOrNull(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity? {
+    return where(realm, roomId, rootThreadEventId).findFirst()
+}
+internal fun RealmList<ThreadSummaryEntity>.find(rootThreadEventId: String): ThreadSummaryEntity? {
+    return this.where()
+            .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+            .findFirst()
+}
+
+internal fun RealmList<ThreadSummaryEntity>.findRootOrLatest(eventId: String): ThreadSummaryEntity? {
+    return this.where()
+            .beginGroup()
+            .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, eventId)
+            .or()
+            .equalTo(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.EVENT_ID, eventId)
+            .endGroup()
+            .findFirst()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
index 63f41ebf2cf47473104e34192f759bf254b1e1fc..81d5ac835f1465b7249a786ab224a3e409eab6cd 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
@@ -97,6 +97,7 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
     if (filters.filterEdits) {
         not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
         not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
+        not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.REFERENCE)
     }
     if (filters.filterRedacted) {
         not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
index 10a0d1dcec01f4bb8dbcd82b7dc86ee62bb1d109..a7317506a0aafbf7e3d05eafb0f2dd1f365d85a0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
@@ -26,6 +26,7 @@ internal object TimelineEventFilter {
     internal object Content {
         internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
         internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}"""
+        internal const val REFERENCE = """{*"m.relates_to"*"rel_type":*"m.reference"*}"""
     }
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt
index 7d004bc5c075514596956145013e8f7195e7a51c..fedd7d05f90117a9a1176c6d9d7a3db7db8f8f9a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt
@@ -80,8 +80,8 @@ internal class WorkManagerProvider @Inject constructor(
             workManager.enqueue(checkWorkerRequest)
             val checkWorkerLiveState = workManager.getWorkInfoByIdLiveData(checkWorkerRequest.id)
             val observer = object : Observer<WorkInfo> {
-                override fun onChanged(workInfo: WorkInfo) {
-                    if (workInfo.state.isFinished) {
+                override fun onChanged(workInfo: WorkInfo?) {
+                    if (workInfo?.state?.isFinished == true) {
                         checkWorkerLiveState.removeObserver(this)
                         if (workInfo.state == WorkInfo.State.FAILED) {
                             throw RuntimeException("MatrixWorkerFactory is not being set on your worker configuration.\n" +
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt
index 3734c5dc1daddace0130b80f91547b511dcf71f0..12adf16cbca399f68e0c770e75d3933c2dcb7c54 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt
@@ -21,3 +21,11 @@ fun <A> Result<A>.foldToCallback(callback: MatrixCallback<A>): Unit = fold(
         { callback.onSuccess(it) },
         { callback.onFailure(it) }
 )
+
+@Suppress("UNCHECKED_CAST") // We're casting null failure results to R
+inline fun <T, R> Result<T>.andThen(block: (T) -> Result<R>): Result<R> {
+    return when (val result = getOrNull()) {
+        null -> this as Result<R>
+        else -> block(result)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java
index 2820b6688617a236a627f6709f959294ca94c90c..62f90f563ef456f868ef0d41d9b0cbd2ba203790 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.legacy.riot;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.SharedPreferences;
 
@@ -196,6 +197,7 @@ public class LoginStorage {
     /**
      * Clear the stored values
      */
+    @SuppressLint("ApplySharedPref")
     public void clear() {
         SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE);
         SharedPreferences.Editor editor = prefs.edit();
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
index 1ab1042129f72743a1b42667723360eaf622d7eb..5aec7db66cc08848103e15bf8b11d081b20c4299 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
@@ -21,6 +21,7 @@ internal object NetworkConstants {
     private const val URI_API_PREFIX_PATH = "_matrix/client"
     const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
     const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
+    const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/"
     const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
 
     // Media
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt
index 6d0cd37e1fa9c2d60b25edba7ff900967d7a4498..93b0dba13e1f7695303363b2561696bd05bc0af4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt
@@ -21,9 +21,9 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import javax.inject.Inject
 
-internal class DefaultCacheService @Inject constructor(@SessionDatabase
-                                                       private val clearCacheTask: ClearCacheTask,
-                                                       private val taskExecutor: TaskExecutor
+internal class DefaultCacheService @Inject constructor(
+        @SessionDatabase private val clearCacheTask: ClearCacheTask,
+        private val taskExecutor: TaskExecutor
 ) : CacheService {
 
     override suspend fun clearCache() {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
index 7415b988a43eadd878a12006e35328445aea2da5..676a4f6a38cfcd9f45b546ca839d0b1fdf5845ec 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
@@ -17,9 +17,21 @@
 package org.matrix.android.sdk.internal.session.filter
 
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import timber.log.Timber
 
 internal object FilterFactory {
 
+    fun createThreadsFilter(numberOfEvents: Int, userId: String?): RoomEventFilter {
+        Timber.i("$userId")
+        return RoomEventFilter(
+                limit = numberOfEvents,
+//                senders = listOf(userId),
+//                relationSenders = userId?.let { listOf(it) },
+                relationTypes = listOf(RelationType.THREAD)
+        )
+    }
+
     fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter {
         return RoomEventFilter(
                 limit = numberOfEvents,
@@ -58,8 +70,8 @@ internal object FilterFactory {
 
     private fun createElementTimelineFilter(): RoomEventFilter? {
         return null // RoomEventFilter().apply {
-            // TODO Enable this for optimization
-            // types = listOfSupportedEventTypes.toMutableList()
+        // TODO Enable this for optimization
+        // types = listOfSupportedEventTypes.toMutableList()
         // }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
index f49832296708f98ff7743224048411e1cbce0b1f..634ea73480a5987b70143b31acea003176ed6725 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
@@ -52,12 +52,13 @@ data class RoomEventFilter(
          * A list of relation types which must be exist pointing to the event being filtered.
          * If this list is absent then no filtering is done on relation types.
          */
-        @Json(name = "relation_types") val relationTypes: List<String>? = null,
+        @Json(name = "related_by_rel_types") val relationTypes: List<String>? = null,
         /**
          *  A list of senders of relations which must exist pointing to the event being filtered.
          *  If this list is absent then no filtering is done on relation types.
          */
-        @Json(name = "relation_senders") val relationSenders: List<String>? = null,
+        @Json(name = "related_by_senders") val relationSenders: List<String>? = null,
+
         /**
          * 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 830a58cd12813398d0df5efe6b1b55892947172a..55526b41db604d0126d0fe69e604bbcccfe3f5f0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
@@ -65,7 +65,13 @@ internal data class Capabilities(
          * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
          */
         @Json(name = "m.room_versions")
-        val roomVersions: RoomVersions? = null
+        val roomVersions: RoomVersions? = null,
+        /**
+         * Capability to indicate if the server supports MSC3440 Threading
+         * True if the user can use m.thread relation, false otherwise
+         */
+        @Json(name = "m.thread")
+        val threads: BooleanCapability? = null
 )
 
 @JsonClass(generateAdapter = true)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index e822cbdcdbf42c06fa2c282dff5b8189400cd8fd..44e13d971a84b3d52a278a1b78783c73e907e75b 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,9 +20,11 @@ import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.api.MatrixPatterns.getDomain
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.orTrue
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
 import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
 import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
 import org.matrix.android.sdk.internal.database.query.getOrCreate
@@ -121,6 +123,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
                 homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
                     MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
                 }
+                homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */
+                        getVersionResult?.doesServerSupportThreads().orFalse()
             }
 
             if (getMediaConfigResult != null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
index 8b05d2ea629adf4db7495235d77eb1546adc5713..8ae203c2b316535342af89aac59f4e41deac80a4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
@@ -56,7 +56,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
 
         val allEvents = (newJoinEvents + inviteEvents).filter { event ->
             when (event.type) {
-                EventType.POLL_START,
+                in EventType.POLL_START,
                 EventType.MESSAGE,
                 EventType.REDACTION,
                 EventType.ENCRYPTED,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
index 144ebb54042dc7774ee9d082a4577a2dade83f6f..196a8c122df8c32d45c6006d7cdb172d7b0e2a3c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
@@ -43,4 +43,8 @@ internal class DefaultPermalinkService @Inject constructor(
     override fun getLinkedId(url: String): String? {
         return permalinkFactory.getLinkedId(url)
     }
+
+    override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
+        return permalinkFactory.createMentionSpanTemplate(type, forceMatrixTo)
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
index 39c1ddfdce3b569f1c5234bf29a9b0cc3642a5b6..0aeb0467de8ebf4ad3f7520448da03becb3a1baa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
@@ -21,7 +21,10 @@ import org.matrix.android.sdk.api.MatrixPatterns
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.permalinks.PermalinkData
 import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService
 import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.HTML
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
 import org.matrix.android.sdk.internal.di.UserId
 import javax.inject.Inject
 
@@ -105,6 +108,23 @@ internal class PermalinkFactory @Inject constructor(
                 ?.substringBeforeLast("?")
     }
 
+    fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
+        return buildString {
+            when (type) {
+                HTML     -> append(MENTION_SPAN_TO_HTML_TEMPLATE_BEGIN)
+                MARKDOWN -> append(MENTION_SPAN_TO_MD_TEMPLATE_BEGIN)
+            }
+            append(baseUrl(forceMatrixTo))
+            if (useClientFormat(forceMatrixTo)) {
+                append(USER_PATH)
+            }
+            when (type) {
+                HTML     -> append(MENTION_SPAN_TO_HTML_TEMPLATE_END)
+                MARKDOWN -> append(MENTION_SPAN_TO_MD_TEMPLATE_END)
+            }
+        }
+    }
+
     /**
      * Escape '/' in id, because it is used as a separator
      *
@@ -147,5 +167,9 @@ internal class PermalinkFactory @Inject constructor(
         private const val ROOM_PATH = "room/"
         private const val USER_PATH = "user/"
         private const val GROUP_PATH = "group/"
+        private const val MENTION_SPAN_TO_HTML_TEMPLATE_BEGIN = "<a href=\""
+        private const val MENTION_SPAN_TO_HTML_TEMPLATE_END = "%1\$s\">%2\$s</a>"
+        private const val MENTION_SPAN_TO_MD_TEMPLATE_BEGIN = "[%2\$s]("
+        private const val MENTION_SPAN_TO_MD_TEMPLATE_END = "%1\$s)"
     }
 }
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 2d8c3e9c78eaecd443d207259b3d0e9998127a9c..34e859e509374e7d4252ef193ce3cc58d6ddc36e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
@@ -36,6 +36,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
 import org.matrix.android.sdk.api.session.room.state.StateService
 import org.matrix.android.sdk.api.session.room.tags.TagsService
 import org.matrix.android.sdk.api.session.room.threads.ThreadsService
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
 import org.matrix.android.sdk.api.session.room.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.typing.TypingService
 import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -56,6 +57,7 @@ internal class DefaultRoom(override val roomId: String,
                            private val roomSummaryDataSource: RoomSummaryDataSource,
                            private val timelineService: TimelineService,
                            private val threadsService: ThreadsService,
+                           private val threadsLocalService: ThreadsLocalService,
                            private val sendService: SendService,
                            private val draftService: DraftService,
                            private val stateService: StateService,
@@ -80,6 +82,7 @@ internal class DefaultRoom(override val roomId: String,
         Room,
         TimelineService by timelineService,
         ThreadsService by threadsService,
+        ThreadsLocalService by threadsLocalService,
         SendService by sendService,
         DraftService by draftService,
         StateService by stateService,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
index 4a02c55db0fe6c007c609f8deaa89324b33ca65c..c79c41069b36dd220302ef4b66287fb920ac3ba5 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
@@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.Transformations
 import androidx.paging.PagedList
 import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.Room
 import org.matrix.android.sdk.api.session.room.RoomService
@@ -32,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.session.room.peeking.PeekResult
+import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
 import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
@@ -51,6 +53,7 @@ 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
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
+import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
 import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
 import org.matrix.android.sdk.internal.util.fetchCopied
 import javax.inject.Inject
@@ -69,6 +72,7 @@ internal class DefaultRoomService @Inject constructor(
         private val roomSummaryDataSource: RoomSummaryDataSource,
         private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
         private val leaveRoomTask: LeaveRoomTask,
+        private val roomSummaryUpdater: RoomSummaryUpdater
 ) : RoomService {
 
     override suspend fun createRoom(createRoomParams: CreateRoomParams): String {
@@ -92,6 +96,23 @@ internal class DefaultRoomService @Inject constructor(
         return roomSummaryDataSource.getRoomSummaries(queryParams, sortOrder)
     }
 
+    override fun refreshJoinedRoomSummaryPreviews(roomId: String?) {
+        val roomSummaries = getRoomSummaries(roomSummaryQueryParams {
+            if (roomId != null) {
+                this.roomId = QueryStringValue.Equals(roomId)
+            }
+            memberships = listOf(Membership.JOIN)
+        })
+
+        if (roomSummaries.isNotEmpty()) {
+            monarchy.runTransactionSync { realm ->
+                roomSummaries.forEach {
+                    roomSummaryUpdater.refreshLatestPreviewContent(realm, it.roomId)
+                }
+            }
+        }
+    }
+
     override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams,
                                       sortOrder: RoomSortOrder): LiveData<List<RoomSummary>> {
         return roomSummaryDataSource.getRoomSummariesLive(queryParams, sortOrder)
@@ -109,6 +130,10 @@ internal class DefaultRoomService @Inject constructor(
         return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
     }
 
+    override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
+        return roomSummaryDataSource.getCountLive(queryParams)
+    }
+
     override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
         return roomSummaryDataSource.getNotificationCountForRooms(queryParams)
     }
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 1e0eb8b49787921d29a5a94bbb9bbe5a04d01fac..8bbe3a9ac61e26f9508ece7b9f80587d983771f4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -57,6 +57,7 @@ import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryE
 import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
 import org.matrix.android.sdk.internal.database.query.create
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.where
@@ -86,11 +87,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             // TODO Add ?
             // EventType.KEY_VERIFICATION_READY,
             EventType.KEY_VERIFICATION_KEY,
-            EventType.ENCRYPTED,
-            EventType.POLL_START,
-            EventType.POLL_RESPONSE,
-            EventType.POLL_END
-    )
+            EventType.ENCRYPTED
+    ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END
 
     override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
         return allowedTypes.contains(eventType)
@@ -117,8 +115,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
 
                         EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
                                 ?.let {
-                                    TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findFirst()
-                                            ?.let { tet -> tet.annotations = it }
+                                    TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
+                                            ?.forEach { tet -> tet.annotations = it }
                                 }
                     }
 
@@ -156,7 +154,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                                 Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
                                 // A replace!
                                 handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
-                            } else if (event.getClearType() == EventType.POLL_RESPONSE) {
+                            } else if (event.getClearType() in EventType.POLL_RESPONSE) {
                                 event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { pollResponseContent ->
                                     Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
                                     handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
@@ -177,12 +175,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                                     handleVerification(realm, event, roomId, isLocalEcho, it)
                                 }
                             }
-                            EventType.POLL_RESPONSE        -> {
+                            in EventType.POLL_RESPONSE     -> {
                                 event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let {
                                     handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId)
                                 }
                             }
-                            EventType.POLL_END             -> {
+                            in EventType.POLL_END          -> {
                                 event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
                                     handleEndPoll(realm, event, it, roomId, isLocalEcho)
                                 }
@@ -196,6 +194,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                             handleReaction(realm, event, roomId, isLocalEcho)
                         }
                     }
+                    // HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations
+//                    else if (event.unsignedData?.relations?.annotations != null) {
+//                        Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}")
+//                        handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
+//                         EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
+//                                 ?.let {
+//                                     TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
+//                                             ?.forEach { tet -> tet.annotations = it }
+//                                 }
+//                    }
                 }
                 EventType.REDACTION            -> {
                     val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
@@ -217,7 +225,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         }
                     }
                 }
-                EventType.POLL_START           -> {
+                in EventType.POLL_START        -> {
                     val content: MessagePollContent? = event.content.toModel()
                     if (content?.relatesTo?.type == RelationType.REPLACE) {
                         Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
@@ -225,12 +233,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         handleReplace(realm, event, content, roomId, isLocalEcho)
                     }
                 }
-                EventType.POLL_RESPONSE        -> {
+                in EventType.POLL_RESPONSE     -> {
                     event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
                         handleResponse(realm, event, it, roomId, isLocalEcho)
                     }
                 }
-                EventType.POLL_END             -> {
+                in EventType.POLL_END          -> {
                     event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
                         handleEndPoll(realm, event, it, roomId, isLocalEcho)
                     }
@@ -243,7 +251,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
     }
 
     // OPT OUT serer aggregation until API mature enough
-    private val SHOULD_HANDLE_SERVER_AGREGGATION = false
+    private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e
 
     private fun handleReplace(realm: Realm,
                               event: Event,
@@ -335,13 +343,18 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
         }
 
         if (!isLocalEcho) {
-            val replaceEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
+            val replaceEvent = TimelineEventEntity
+                    .where(realm, roomId, eventId)
+                    .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
+                    .findFirst()
             handleThreadSummaryEdition(editedEvent, replaceEvent, existingSummary?.editions)
         }
     }
 
     /**
      * Check if the edition is on the latest thread event, and update it accordingly
+     * @param editedEvent The event that will be changed
+     * @param replaceEvent The new event
      */
     private fun handleThreadSummaryEdition(editedEvent: EventEntity?,
                                            replaceEvent: TimelineEventEntity?,
@@ -407,12 +420,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             return
         }
 
-        val option = content.response?.answers?.first() ?: return Unit.also {
+        val option = content.getBestResponse()?.answers?.first() ?: return Unit.also {
             Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
         }
 
         // Check if this option is in available options
-        if (!targetPollContent.pollCreationInfo?.answers?.map { it.id }?.contains(option).orFalse()) {
+        if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(option).orFalse()) {
             Timber.v("## POLL $targetEventId doesn't contain option $option")
             return
         }
@@ -469,46 +482,39 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                               roomId: String,
                               isLocalEcho: Boolean) {
         val pollEventId = content.relatesTo?.eventId ?: return
-
         val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId
         val isPollOwner = pollOwnerId == event.senderId
-
         val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
                 ?.content?.toModel<PowerLevelsContent>()
                 ?.let { PowerLevelsHelper(it) }
+
         if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) {
             Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId")
             return
         }
 
-        var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
-        if (existing == null) {
+        var existingPoll = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
+        if (existingPoll == null) {
             Timber.v("## POLL creating new relation summary for $pollEventId")
-            existing = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
+            existingPoll = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
         }
 
         // we have it
-        val existingPollSummary = existing.pollResponseSummary
+        val existingPollSummary = existingPoll.pollResponseSummary
                 ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also {
-                    existing.pollResponseSummary = it
+                    existingPoll.pollResponseSummary = it
                 }
 
-        if (existingPollSummary.closedTime != null) {
-            Timber.v("## Received poll.end event for already ended poll $pollEventId")
-            return
-        }
-
         val txId = event.unsignedData?.transactionId
+        existingPollSummary.closedTime = event.originServerTs
+
         // is it a remote echo?
         if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
             // ok it has already been managed
             Timber.v("## POLL  Receiving remote echo of response eventId:$pollEventId")
             existingPollSummary.sourceLocalEchoEvents.remove(txId)
             existingPollSummary.sourceEvents.add(event.eventId)
-            return
         }
-
-        existingPollSummary.closedTime = event.originServerTs
     }
 
     private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? {
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 399bfbd0e45b00e64fb790b2186241b4768f160f..10f75473b71e3256c85e486bcf9238a4b6bf57f2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room
 
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.RelationType
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomStrippedState
 import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
@@ -86,7 +87,7 @@ internal interface RoomAPI {
     suspend fun getRoomMessagesFrom(@Path("roomId") roomId: String,
                                     @Query("from") from: String,
                                     @Query("dir") dir: String,
-                                    @Query("limit") limit: Int,
+                                    @Query("limit") limit: Int?,
                                     @Query("filter") filter: String?
     ): PaginationResponse
 
@@ -218,7 +219,6 @@ internal interface RoomAPI {
 
     /**
      * Paginate relations for event based in normal topological order
-     *
      * @param relationType filter for this relation type
      * @param eventType filter for this event type
      */
@@ -227,9 +227,24 @@ internal interface RoomAPI {
                              @Path("eventId") eventId: String,
                              @Path("relationType") relationType: String,
                              @Path("eventType") eventType: String,
+                             @Query("from") from: String? = null,
+                             @Query("to") to: String? = null,
                              @Query("limit") limit: Int? = null
     ): RelationsResponse
 
+    /**
+     * Paginate relations for thread events based in normal topological order
+     * @param relationType filter for this relation type
+     */
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
+    suspend fun getThreadsRelations(@Path("roomId") roomId: String,
+                                    @Path("eventId") eventId: String,
+                                    @Path("relationType") relationType: String = RelationType.THREAD,
+                                    @Query("from") from: String? = null,
+                                    @Query("to") to: String? = null,
+                                    @Query("limit") limit: Int? = null
+    ): RelationsResponse
+
     /**
      * Join the given room.
      *
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
index 70c1ab4f42453412aa2a5b258b3979121bdcd966..72a3f9ab22036910d5624c0aa109fc47ab194cad 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
@@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.room.state.SendStateTask
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
 import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService
 import org.matrix.android.sdk.internal.session.room.threads.DefaultThreadsService
+import org.matrix.android.sdk.internal.session.room.threads.local.DefaultThreadsLocalService
 import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService
 import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
 import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
@@ -52,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
                                                       private val roomSummaryDataSource: RoomSummaryDataSource,
                                                       private val timelineServiceFactory: DefaultTimelineService.Factory,
                                                       private val threadsServiceFactory: DefaultThreadsService.Factory,
+                                                      private val threadsLocalServiceFactory: DefaultThreadsLocalService.Factory,
                                                       private val sendServiceFactory: DefaultSendService.Factory,
                                                       private val draftServiceFactory: DefaultDraftService.Factory,
                                                       private val stateServiceFactory: DefaultStateService.Factory,
@@ -79,6 +81,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
                 roomSummaryDataSource = roomSummaryDataSource,
                 timelineService = timelineServiceFactory.create(roomId),
                 threadsService = threadsServiceFactory.create(roomId),
+                threadsLocalService = threadsLocalServiceFactory.create(roomId),
                 sendService = sendServiceFactory.create(roomId),
                 draftService = draftServiceFactory.create(roomId),
                 stateService = stateServiceFactory.create(roomId),
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
index f831a77a5d7c72539c8f4339309b62206caa1d41..5e90076b8af24fb73116c5d52fc9af1830cd56dc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
@@ -77,7 +77,9 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
 import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
 import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
 import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask
 import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
 import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
 import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
@@ -294,4 +296,7 @@ internal abstract class RoomModule {
 
     @Binds
     abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask
+
+    @Binds
+    abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
index ee52fe574b932ba0cf0aaac736746a51f32b5a0a..b19b8d4a6b6aaef24fdc02eb6fa7a446de7bcc88 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
@@ -21,11 +21,14 @@ 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.LocalEcho
 import org.matrix.android.sdk.api.session.events.model.UnsignedData
+import org.matrix.android.sdk.internal.database.helper.countInThreadMessages
+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.EventEntity
 import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 import org.matrix.android.sdk.internal.database.query.findWithSenderMembershipEvent
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -71,7 +74,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
             when (typeToPrune) {
                 EventType.ENCRYPTED,
                 EventType.MESSAGE,
-                EventType.POLL_START -> {
+                in EventType.POLL_START -> {
                     Timber.d("REDACTION for message ${eventToPrune.eventId}")
                     val unsignedData = EventMapper.map(eventToPrune).unsignedData
                             ?: UnsignedData(null, null)
@@ -89,6 +92,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
                     eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
                     eventToPrune.decryptionResultJson = null
                     eventToPrune.decryptionErrorCode = null
+
+                    handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho)
                 }
 //                EventType.REACTION -> {
 //                    eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
@@ -104,6 +109,39 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
         }
     }
 
+    /**
+     * Invalidates the number of threads in the main timeline thread summary,
+     * with respect to redactions.
+     */
+    private fun handleTimelineThreadSummaryIfNeeded(
+            realm: Realm,
+            eventToPrune: EventEntity,
+            isLocalEcho: Boolean,
+    ) {
+        if (eventToPrune.isThread() && !isLocalEcho) {
+            val roomId = eventToPrune.roomId
+            val rootThreadEvent = eventToPrune.findRootThreadEvent() ?: return
+            val rootThreadEventId = eventToPrune.rootThreadEventId ?: return
+
+            val inThreadMessages = countInThreadMessages(
+                    realm = realm,
+                    roomId = roomId,
+                    rootThreadEventId = rootThreadEventId
+            )
+
+            rootThreadEvent.numberOfThreads = inThreadMessages
+            if (inThreadMessages == 0) {
+                // We should also clear the thread summary list
+                rootThreadEvent.isRootThread = false
+                rootThreadEvent.threadSummaryLatestMessage = null
+                ThreadSummaryEntity
+                        .where(realm, roomId = roomId, rootThreadEventId)
+                        .findFirst()
+                        ?.deleteFromRealm()
+            }
+        }
+    }
+
     private fun computeAllowedKeys(type: String): List<String> {
         // Add filtered content, allowed keys in content depends on the event type
         return when (type) {
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 d5019aea7b842448b91636324db1720b9333511e..ab514d31c8480951f7ff567eb803562566aba1e8 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
@@ -34,7 +34,6 @@ 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.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.session.room.timeline.TimelineEventDataSource
@@ -48,7 +47,6 @@ internal class DefaultRelationService @AssistedInject constructor(
         private val eventFactory: LocalEchoEventFactory,
         private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
         private val fetchEditHistoryTask: FetchEditHistoryTask,
-        private val fetchThreadTimelineTask: FetchThreadTimelineTask,
         private val timelineEventDataSource: TimelineEventDataSource,
         @SessionDatabase private val monarchy: Monarchy
 ) : RelationService {
@@ -196,10 +194,6 @@ internal class DefaultRelationService @AssistedInject constructor(
         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/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b596f2288ec82f849ca708b5a0a7225ea83cff78
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.matrix.android.sdk.internal.session.room.relation.threads
+
+import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
+import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.database.helper.createOrUpdate
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.filter.FilterFactory
+import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
+import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import timber.log.Timber
+import javax.inject.Inject
+
+/***
+ * This class is responsible to Fetch all the thread in the current room,
+ * To fetch all threads in a room, the /messages API is used with newly added filtering options.
+ */
+internal interface FetchThreadSummariesTask : Task<FetchThreadSummariesTask.Params, DefaultFetchThreadSummariesTask.Result> {
+    data class Params(
+            val roomId: String,
+            val from: String = "",
+            val limit: Int = 500,
+            val isUserParticipating: Boolean = true
+    )
+}
+
+internal class DefaultFetchThreadSummariesTask @Inject constructor(
+        private val roomAPI: RoomAPI,
+        private val globalErrorReceiver: GlobalErrorReceiver,
+        @SessionDatabase private val monarchy: Monarchy,
+        private val cryptoService: DefaultCryptoService,
+        @UserId private val userId: String,
+) : FetchThreadSummariesTask {
+
+    override suspend fun execute(params: FetchThreadSummariesTask.Params): Result {
+        val filter = FilterFactory.createThreadsFilter(
+                numberOfEvents = params.limit,
+                userId = if (params.isUserParticipating) userId else null).toJSONString()
+
+        val response = executeRequest(
+                globalErrorReceiver,
+                canRetry = true
+        ) {
+            roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter)
+        }
+
+        Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ")
+
+        return handleResponse(response, params)
+    }
+
+    private suspend fun handleResponse(response: PaginationResponse,
+                                       params: FetchThreadSummariesTask.Params): Result {
+        val rootThreadList = response.events
+        monarchy.awaitTransaction { realm ->
+            val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction
+
+            val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
+            for (rootThreadEvent in rootThreadList) {
+                if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) {
+                    continue
+                }
+
+                ThreadSummaryEntity.createOrUpdate(
+                        threadSummaryType = ThreadSummaryUpdateType.REPLACE,
+                        realm = realm,
+                        roomId = params.roomId,
+                        rootThreadEvent = rootThreadEvent,
+                        roomMemberContentsByUser = roomMemberContentsByUser,
+                        roomEntity = roomEntity,
+                        userId = userId,
+                        cryptoService = cryptoService)
+            }
+        }
+        return Result.SUCCESS
+    }
+
+    enum class Result {
+        SHOULD_FETCH_MORE,
+        REACHED_END,
+        SUCCESS
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index e0d501c515f7ea3ec595227d813c9d62a0f5fa6a..a46bbe8d9f8b76e1111db920b08f05b1b8737940 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Matrix.org Foundation C.I.C.
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,14 +20,12 @@ 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
@@ -36,8 +34,10 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
-import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
+import org.matrix.android.sdk.internal.database.query.find
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.getOrNull
 import org.matrix.android.sdk.internal.database.query.where
@@ -47,16 +47,38 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
 import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
 import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
 import org.matrix.android.sdk.internal.task.Task
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import timber.log.Timber
 import javax.inject.Inject
 
-internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, Boolean> {
+/***
+ * This class is responsible to Fetch paginated chunks of the thread timeline using the /relations API
+ *
+ * How it works
+ *
+ * The problem?
+ *  - We cannot use the existing timeline architecture to paginate through the timeline
+ *  - We want our new events to be live, so any interactions with them like reactions will continue to work. We should
+ *    handle appropriately the existing events from /messages api with the new events from /relations.
+ *  - Handling edge cases like receiving an event from /messages while you have already created a new one from the /relations response
+ *
+ * The solution
+ * We generate a temporarily thread chunk that will be used to store any new paginated results from the /relations api
+ * We bind the timeline events from that chunk with the already existing ones. So we will have one common instance, and
+ * all reactions, edits etc will continue to work. If the events do not exists we create them
+ * and we will reuse the same EventEntity instance when (and if) the same event will be fetched from the main (/messages) timeline
+ *
+ */
+internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, DefaultFetchThreadTimelineTask.Result> {
     data class Params(
             val roomId: String,
-            val rootThreadEventId: String
+            val rootThreadEventId: String,
+            val from: String?,
+            val limit: Int
+
     )
 }
 
@@ -69,94 +91,130 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
         private val cryptoService: DefaultCryptoService
 ) : FetchThreadTimelineTask {
 
-    override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
-        val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
+    enum class Result {
+        SHOULD_FETCH_MORE,
+        REACHED_END,
+        SUCCESS
+    }
+
+    override suspend fun execute(params: FetchThreadTimelineTask.Params): Result {
         val response = executeRequest(globalErrorReceiver) {
-            roomAPI.getRelations(
+            roomAPI.getThreadsRelations(
                     roomId = params.roomId,
                     eventId = params.rootThreadEventId,
-                    relationType = RelationType.IO_THREAD,
-                    eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE,
-                    limit = 2000
+                    from = params.from,
+                    limit = params.limit
             )
         }
 
-        val threadList = response.chunks + listOfNotNull(response.originalEvent)
-
-        return storeNewEventsIfNeeded(threadList, params.roomId)
+        Timber.i("###THREADS FetchThreadTimelineTask Fetched size:${response.chunks.size} nextBatch:${response.nextBatch} ")
+        return handleRelationsResponse(response, params)
     }
 
-    /**
-     * Store new events if they are not already received, and returns weather or not,
-     * a timeline update should be made
-     * @param threadList is the list containing the thread replies
-     * @param roomId the roomId of the the thread
-     * @return
-     */
-    private suspend fun storeNewEventsIfNeeded(threadList: List<Event>, roomId: String): Boolean {
-        var eventsSkipped = 0
-        monarchy
-                .awaitTransaction { realm ->
-                    val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
-
-                    val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
-                    val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
-
-                    for (event in threadList.reversed()) {
-                        if (event.eventId == null || event.senderId == null || event.type == null) {
-                            eventsSkipped++
-                            continue
-                        }
-
-                        if (EventEntity.where(realm, event.eventId).findFirst() != null) {
-                            //  Skip if event already exists
-                            eventsSkipped++
-                            continue
-                        }
-                        if (event.isEncrypted()) {
-                            // Decrypt events that will be stored
-                            decryptIfNeeded(event, roomId)
-                        }
-
-                        handleReaction(realm, event, roomId)
-
-                        val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
-                        val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
-
-                        // Sender info
-                        roomMemberContentsByUser.getOrPut(event.senderId) {
-                            // If we don't have any new state on this user, get it from db
-                            val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
-                            rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
-                        }
-
-                        chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
-                        eventEntity.rootThreadEventId?.let {
-                            // This is a thread event
-                            optimizedThreadSummaryMap[it] = eventEntity
-                        } ?: run {
-                            // This is a normal event or a root thread one
-                            optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
-                        }
+    private suspend fun handleRelationsResponse(response: RelationsResponse,
+                                                params: FetchThreadTimelineTask.Params): Result {
+        val threadList = response.chunks
+        val threadRootEvent = response.originalEvent
+        val hasReachEnd = response.nextBatch == null
+
+        monarchy.awaitTransaction { realm ->
+
+            val threadChunk = ChunkEntity.findLastForwardChunkOfThread(realm, params.roomId, params.rootThreadEventId)
+                    ?: run {
+                        return@awaitTransaction
                     }
 
-                    optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
-                            roomId = roomId,
-                            realm = realm,
-                            currentUserId = userId,
-                            shouldUpdateNotifications = false
-                    )
+            threadChunk.prevToken = response.nextBatch
+            val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
+
+            for (event in threadList) {
+                if (event.eventId == null || event.senderId == null || event.type == null) {
+                    continue
+                }
+
+                if (threadChunk.timelineEvents.find(event.eventId) != null) {
+                    // Event already exists in thread chunk, skip it
+                    Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} already exists in thread chunk, skip it")
+                    continue
+                }
+
+                val timelineEvent = TimelineEventEntity
+                        .where(realm, roomId = params.roomId, event.eventId)
+                        .findFirst()
+
+                if (timelineEvent != null) {
+                    // Event already exists but not in the thread chunk
+                    // Lets added there
+                    Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} exists but not in the thread chunk, add it at the end")
+                    threadChunk.timelineEvents.add(timelineEvent)
+                } else {
+                    Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} is brand NEW create an entity and add it!")
+                    val eventEntity = createEventEntity(params.roomId, event, realm)
+                    roomMemberContentsByUser.addSenderState(realm, params.roomId, event.senderId)
+                    threadChunk.addTimelineEvent(
+                            roomId = params.roomId,
+                            eventEntity = eventEntity,
+                            direction = PaginationDirection.FORWARDS,
+                            ownedByThreadChunk = true,
+                            roomMemberContentsByUser = roomMemberContentsByUser)
                 }
-        Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}")
+            }
 
-        return eventsSkipped == threadList.size
+            if (hasReachEnd) {
+                val rootThread = TimelineEventEntity
+                        .where(realm, roomId = params.roomId, params.rootThreadEventId)
+                        .findFirst()
+                if (rootThread != null) {
+                    // If root thread event already exists add it to our chunk
+                    threadChunk.timelineEvents.add(rootThread)
+                    Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} found and added!")
+                } else if (threadRootEvent?.senderId != null) {
+                    // Case when thread event is not in the device
+                    Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} NOT FOUND! Lets create a temp one")
+                    val eventEntity = createEventEntity(params.roomId, threadRootEvent, realm)
+                    roomMemberContentsByUser.addSenderState(realm, params.roomId, threadRootEvent.senderId)
+                    threadChunk.addTimelineEvent(
+                            roomId = params.roomId,
+                            eventEntity = eventEntity,
+                            direction = PaginationDirection.FORWARDS,
+                            ownedByThreadChunk = true,
+                            roomMemberContentsByUser = roomMemberContentsByUser)
+                }
+            }
+        }
+
+        return if (hasReachEnd) {
+            Result.REACHED_END
+        } else {
+            Result.SHOULD_FETCH_MORE
+        }
     }
 
+    // TODO Reuse this function to all the app
     /**
-     * Invoke the event decryption mechanism for a specific event
+     * If we don't have any new state on this user, get it from db
      */
+    private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roomId: String, senderId: String) {
+        getOrPut(senderId) {
+            CurrentStateEventEntity
+                    .getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)
+                    ?.root?.asDomain()
+                    ?.getFixedRoomMemberContent()
+        }
+    }
 
-    private fun decryptIfNeeded(event: Event, roomId: String) {
+    /**
+     * Create an EventEntity to be added in the TimelineEventEntity
+     */
+    private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
+        val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+        return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+    }
+
+    /**
+     * Invoke the event decryption mechanism for a specific event
+     */
+    private suspend 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), "")
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 28c17f38b6a51cf881d1d19c6aea439f246d5fd6..31c7254ed5ee77a3f443f6248f3a8087746e2bc0 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
@@ -128,8 +128,8 @@ internal class DefaultSendService @AssistedInject constructor(
                 .let { sendEvent(it) }
     }
 
-    override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
-        return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty)
+    override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
+        return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty, isUserLocation)
                 .also { createLocalEcho(it) }
                 .let { sendEvent(it) }
     }
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 3c36d587107c55df72f09177a2cbafc6233359fd..0ba95cc1fb834f5426cd0ce481d95cfd4dce5968 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
@@ -137,16 +137,11 @@ internal class LocalEchoEventFactory @Inject constructor(
                                   options: List<String>,
                                   pollType: PollType): MessagePollContent {
         return MessagePollContent(
-                pollCreationInfo = PollCreationInfo(
-                        question = PollQuestion(
-                                question = question
-                        ),
+                unstablePollCreationInfo = PollCreationInfo(
+                        question = PollQuestion(unstableQuestion = question),
                         kind = pollType,
                         answers = options.map { option ->
-                            PollAnswer(
-                                    id = UUID.randomUUID().toString(),
-                                    answer = option
-                            )
+                            PollAnswer(id = UUID.randomUUID().toString(), unstableAnswer = option)
                         }
                 )
         )
@@ -167,7 +162,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START,
+                type = EventType.POLL_START.first(),
                 content = newContent.toContent()
         )
     }
@@ -179,11 +174,9 @@ internal class LocalEchoEventFactory @Inject constructor(
                 body = answerId,
                 relatesTo = RelationDefaultContent(
                         type = RelationType.REFERENCE,
-                        eventId = pollEventId),
-                response = PollResponse(
-                        answers = listOf(answerId)
-                )
-
+                        eventId = pollEventId
+                ),
+                unstableResponse = PollResponse(answers = listOf(answerId))
         )
         val localId = LocalEcho.createLocalEchoId()
         return Event(
@@ -191,7 +184,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_RESPONSE,
+                type = EventType.POLL_RESPONSE.first(),
                 content = content.toContent(),
                 unsignedData = UnsignedData(age = null, transactionId = localId))
     }
@@ -207,7 +200,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START,
+                type = EventType.POLL_START.first(),
                 content = content.toContent(),
                 unsignedData = UnsignedData(age = null, transactionId = localId))
     }
@@ -226,7 +219,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_END,
+                type = EventType.POLL_END.first(),
                 content = content.toContent(),
                 unsignedData = UnsignedData(age = null, transactionId = localId))
     }
@@ -234,20 +227,17 @@ internal class LocalEchoEventFactory @Inject constructor(
     fun createLocationEvent(roomId: String,
                             latitude: Double,
                             longitude: Double,
-                            uncertainty: Double?): Event {
+                            uncertainty: Double?,
+                            isUserLocation: Boolean): Event {
         val geoUri = buildGeoUri(latitude, longitude, uncertainty)
+        val assetType = if (isUserLocation) LocationAssetType.SELF else LocationAssetType.PIN
         val content = MessageLocationContent(
                 geoUri = geoUri,
                 body = geoUri,
-                locationInfo = LocationInfo(
-                        geoUri = geoUri,
-                        description = geoUri
-                ),
-                locationAsset = LocationAsset(
-                        type = LocationAssetType.SELF
-                ),
-                ts = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
-                text = geoUri
+                unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri),
+                unstableLocationAsset = LocationAsset(type = assetType),
+                unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
+                unstableText = geoUri
         )
         return createMessageEvent(roomId, content)
     }
@@ -353,8 +343,9 @@ internal class LocalEchoEventFactory @Inject constructor(
                 url = attachment.queryUri.toString(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
@@ -396,8 +387,9 @@ internal class LocalEchoEventFactory @Inject constructor(
                 url = attachment.queryUri.toString(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
@@ -426,8 +418,9 @@ internal class LocalEchoEventFactory @Inject constructor(
                 voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
@@ -446,8 +439,9 @@ internal class LocalEchoEventFactory @Inject constructor(
                 url = attachment.queryUri.toString(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
@@ -479,7 +473,7 @@ internal class LocalEchoEventFactory @Inject constructor(
     private fun enhanceStickerIfNeeded(type: String, content: Content?): Content? {
         var newContent: Content? = null
         if (type == EventType.STICKER) {
-            val isThread = (content.toModel<MessageStickerContent>())?.relatesTo?.type == RelationType.IO_THREAD
+            val isThread = (content.toModel<MessageStickerContent>())?.relatesTo?.type == RelationType.THREAD
             val rootThreadEventId = (content.toModel<MessageStickerContent>())?.relatesTo?.eventId
             if (isThread && rootThreadEventId != null) {
                 val newRelationalDefaultContent = (content.toModel<MessageStickerContent>())?.relatesTo?.copy(
@@ -560,7 +554,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 relatesTo = generateReplyRelationContent(
                         eventId = eventId,
                         rootThreadEventId = rootThreadEventId,
-                        showAsReply = showInThread))
+                        showInThread = showInThread))
         return createMessageEvent(roomId, content)
     }
 
@@ -570,18 +564,20 @@ internal class LocalEchoEventFactory @Inject constructor(
      * "m.relates_to": {
      *      "rel_type": "m.thread",
      *      "event_id": "$thread_root",
+     *      "is_falling_back": false,
      *      "m.in_reply_to": {
-     *          "event_id": "$event_target",
-     *          "render_in": ["m.thread"]
-     *        }
-     *   }
+     *          "event_id": "$event_target"
+     *      }
+     *  }
      */
-    private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent =
+    private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showInThread: Boolean): RelationDefaultContent =
             rootThreadEventId?.let {
                 RelationDefaultContent(
-                        type = RelationType.IO_THREAD,
+                        type = RelationType.THREAD,
                         eventId = it,
-                        inReplyTo = ReplyToContent(eventId = eventId, renderIn = if (showAsReply) arrayListOf("m.thread") else null))
+                        isFallingBack = showInThread,
+                        // False when is a rich reply from within a thread, and true when is a reply that should be visible from threads
+                        inReplyTo = ReplyToContent(eventId = eventId))
             } ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId))
 
     private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
@@ -638,7 +634,9 @@ internal class LocalEchoEventFactory @Inject constructor(
             MessageType.MSGTYPE_AUDIO      -> return TextContent("sent an audio file.")
             MessageType.MSGTYPE_IMAGE      -> return TextContent("sent an image.")
             MessageType.MSGTYPE_VIDEO      -> return TextContent("sent a video.")
-            MessageType.MSGTYPE_POLL_START -> return TextContent((content as? MessagePollContent)?.pollCreationInfo?.question?.question ?: "")
+            MessageType.MSGTYPE_POLL_START -> {
+                return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
+            }
             else                           -> return TextContent(content?.body ?: "")
         }
     }
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 5c629f87f0e31608e79b6a6ee7a98dfcf2bf354d..93c0167abe8eeadf5822117dcf8bed0edd324c70 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt
@@ -58,8 +58,9 @@ fun TextContent.toThreadTextContent(
             format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
             body = text,
             relatesTo = RelationDefaultContent(
-                    type = RelationType.IO_THREAD,
+                    type = RelationType.THREAD,
                     eventId = rootThreadEventId,
+                    isFallingBack = true,
                     inReplyTo = ReplyToContent(
                             eventId = latestThreadEventId
                     )),
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
index ccbfbfcded2ab238efe4fff13ee6f84f93b81bf5..fa2e0052abd8c197a934da65cb366074c438d2be 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
@@ -16,6 +16,7 @@
 package org.matrix.android.sdk.internal.session.room.send.pills
 
 import android.text.SpannableString
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService
 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
@@ -28,7 +29,8 @@ import javax.inject.Inject
  */
 internal class TextPillsUtils @Inject constructor(
         private val mentionLinkSpecComparator: MentionLinkSpecComparator,
-        private val displayNameResolver: DisplayNameResolver
+        private val displayNameResolver: DisplayNameResolver,
+        private val permalinkService: PermalinkService
 ) {
 
     /**
@@ -36,7 +38,7 @@ internal class TextPillsUtils @Inject constructor(
      * @return the transformed String or null if no Span found
      */
     fun processSpecialSpansToHtml(text: CharSequence): String? {
-        return transformPills(text, MENTION_SPAN_TO_HTML_TEMPLATE)
+        return transformPills(text, permalinkService.createMentionSpanTemplate(PermalinkService.SpanTemplateType.HTML))
     }
 
     /**
@@ -44,7 +46,7 @@ internal class TextPillsUtils @Inject constructor(
      * @return the transformed String or null if no Span found
      */
     fun processSpecialSpansToMarkdown(text: CharSequence): String? {
-        return transformPills(text, MENTION_SPAN_TO_MD_TEMPLATE)
+        return transformPills(text, permalinkService.createMentionSpanTemplate(PermalinkService.SpanTemplateType.MARKDOWN))
     }
 
     private fun transformPills(text: CharSequence, template: String): String? {
@@ -108,10 +110,4 @@ internal class TextPillsUtils @Inject constructor(
             i++
         }
     }
-
-    companion object {
-        private const val MENTION_SPAN_TO_HTML_TEMPLATE = "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>"
-
-        private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)"
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
index c9fc3c9575be324d9efcb63c07147fbc51ae6835..18a4f80547cb23e23914a686d466b8967bd0994c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
@@ -26,6 +26,7 @@ import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import io.realm.RealmQuery
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.query.ActiveSpaceFilter
 import org.matrix.android.sdk.api.query.RoomCategoryFilter
 import org.matrix.android.sdk.api.query.isNormalized
@@ -42,6 +43,7 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification
 import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
+import org.matrix.android.sdk.internal.database.RealmSessionProvider
 import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
@@ -55,8 +57,10 @@ import javax.inject.Inject
 
 internal class RoomSummaryDataSource @Inject constructor(
         @SessionDatabase private val monarchy: Monarchy,
+        private val realmSessionProvider: RealmSessionProvider,
         private val roomSummaryMapper: RoomSummaryMapper,
-        private val queryStringValueProcessor: QueryStringValueProcessor
+        private val queryStringValueProcessor: QueryStringValueProcessor,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers
 ) {
 
     fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
@@ -219,14 +223,25 @@ internal class RoomSummaryDataSource @Inject constructor(
         return object : UpdatableLivePageResult {
             override val livePagedList: LiveData<PagedList<RoomSummary>> = mapped
 
-            override fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) {
-                realmDataSourceFactory.updateQuery {
-                    roomSummariesQuery(it, builder.invoke(queryParams)).process(sortOrder)
-                }
-            }
-
             override val liveBoundaries: LiveData<ResultBoundaries>
                 get() = boundaries
+
+            override var queryParams: RoomSummaryQueryParams = queryParams
+                set(value) {
+                    field = value
+                    realmDataSourceFactory.updateQuery {
+                        roomSummariesQuery(it, value).process(sortOrder)
+                    }
+                }
+        }
+    }
+
+    fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
+        val liveRooms = monarchy.findAllManagedWithChanges {
+            roomSummariesQuery(it, queryParams)
+        }
+        return Transformations.map(liveRooms) {
+            it.realmResults.where().count().toInt()
         }
     }
 
@@ -293,6 +308,7 @@ internal class RoomSummaryDataSource @Inject constructor(
             RoomCategoryFilter.ONLY_ROOMS              -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
             RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0)
             RoomCategoryFilter.ALL                     -> Unit // nop
+            null                                       -> Unit
         }
 
         // Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}")
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 1c1d59fb3d23f119b0d5d87426adf1244b156bb1..c9d84b1b9357f4c631e2e2699cfd497caad98c1b 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
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.summary
 
 import io.realm.Realm
 import io.realm.kotlin.createObject
+import kotlinx.coroutines.runBlocking
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.EventType
@@ -63,7 +64,6 @@ import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataD
 import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
 import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
 import org.matrix.android.sdk.internal.session.room.relationship.RoomChildRelationInfo
-import org.matrix.android.sdk.internal.util.Normalizer
 import timber.log.Timber
 import javax.inject.Inject
 import kotlin.system.measureTimeMillis
@@ -74,8 +74,16 @@ internal class RoomSummaryUpdater @Inject constructor(
         private val roomAvatarResolver: RoomAvatarResolver,
         private val eventDecryptor: EventDecryptor,
         private val crossSigningService: DefaultCrossSigningService,
-        private val roomAccountDataDataSource: RoomAccountDataDataSource,
-        private val normalizer: Normalizer) {
+        private val roomAccountDataDataSource: RoomAccountDataDataSource
+) {
+
+    fun refreshLatestPreviewContent(realm: Realm, roomId: String) {
+        val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId)
+        if (roomSummaryEntity != null) {
+            val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
+            latestPreviewableEvent?.attemptToDecrypt()
+        }
+    }
 
     fun update(realm: Realm,
                roomId: String,
@@ -127,6 +135,7 @@ internal class RoomSummaryUpdater @Inject constructor(
         val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs
         if (lastActivityFromEvent != null) {
             roomSummaryEntity.lastActivityTime = lastActivityFromEvent
+            latestPreviewableEvent.attemptToDecrypt()
         }
 
         roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 ||
@@ -160,16 +169,6 @@ internal class RoomSummaryUpdater @Inject constructor(
         }
         roomSummaryEntity.updateHasFailedSending()
 
-        val root = latestPreviewableEvent?.root
-        if (root?.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
-            Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
-            // mmm i want to decrypt now or is it ok to do it async?
-            tryOrNull {
-                eventDecryptor.decryptEvent(root.asDomain(), "")
-            }
-                    ?.let { root.setDecryptionResult(it) }
-        }
-
         if (updateMembers) {
             val otherRoomMembers = RoomMemberHelper(realm, roomId)
                     .queryActiveRoomMembersEvent()
@@ -186,6 +185,22 @@ internal class RoomSummaryUpdater @Inject constructor(
         }
     }
 
+    private fun TimelineEventEntity.attemptToDecrypt() {
+        when (val root = this.root) {
+            null -> {
+                Timber.v("Decryption skipped due to missing root event $eventId")
+            }
+            else -> {
+                if (root.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
+                    Timber.v("Should decrypt $eventId")
+                    tryOrNull {
+                        runBlocking { eventDecryptor.decryptEvent(root.asDomain(), "") }
+                    }?.let { root.setDecryptionResult(it) }
+                }
+            }
+        }
+    }
+
     private fun RoomSummaryEntity.updateHasFailedSending() {
         hasFailedSending = TimelineEventEntity.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES).isNotEmpty()
     }
@@ -411,8 +426,6 @@ internal class RoomSummaryUpdater @Inject constructor(
                         realm.where(RoomSummaryEntity::class.java)
                                 .process(RoomSummaryEntityFields.MEMBERSHIP_STR, listOf(Membership.JOIN))
                                 .notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE)
-                                // also we do not count DM in here, because home space will already show them
-                                .equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
                                 .contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, space.roomId)
                                 .findAll().forEach {
                                     highlightCount += it.highlightCount
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
index 5967ae8d2edd2a11d7653fe52ea95565ccd75364..b65991347dd3693edab907d8c211243e3c72c544 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
@@ -23,25 +23,25 @@ import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.room.threads.ThreadsService
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
-import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
-import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions
 import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
-import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
-import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
+import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
-import org.matrix.android.sdk.internal.database.model.EventEntity
-import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
-import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.UserId
-import org.matrix.android.sdk.internal.util.awaitTransaction
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 
 internal class DefaultThreadsService @AssistedInject constructor(
         @Assisted private val roomId: String,
         @UserId private val userId: String,
+        private val fetchThreadTimelineTask: FetchThreadTimelineTask,
+        private val fetchThreadSummariesTask: FetchThreadSummariesTask,
         @SessionDatabase private val monarchy: Monarchy,
         private val timelineEventMapper: TimelineEventMapper,
+        private val threadSummaryMapper: ThreadSummaryMapper
 ) : ThreadsService {
 
     @AssistedFactory
@@ -49,55 +49,40 @@ internal class DefaultThreadsService @AssistedInject constructor(
         fun create(roomId: String): DefaultThreadsService
     }
 
-    override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
+    override fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>> {
         return monarchy.findAllMappedWithChanges(
-                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
+                { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                {
+                    threadSummaryMapper.map(it)
+                }
         )
     }
 
-    override fun getMarkedThreadNotifications(): List<TimelineEvent> {
+    override fun getAllThreadSummaries(): List<ThreadSummary> {
         return monarchy.fetchAllMappedSync(
-                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
+                { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                { threadSummaryMapper.map(it) }
         )
     }
 
-    override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
-        return monarchy.findAllMappedWithChanges(
-                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
-        )
-    }
-
-    override fun getAllThreads(): List<TimelineEvent> {
-        return monarchy.fetchAllMappedSync(
-                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
-        )
-    }
-
-    override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
+    override fun enhanceThreadWithEditions(threads: List<ThreadSummary>): List<ThreadSummary> {
         return Realm.getInstance(monarchy.realmConfiguration).use {
-            TimelineEventEntity.isUserParticipatingInThread(
-                    realm = it,
-                    roomId = roomId,
-                    rootThreadEventId = rootThreadEventId,
-                    senderId = userId)
+            threads.enhanceWithEditions(it, roomId)
         }
     }
 
-    override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> {
-        return Realm.getInstance(monarchy.realmConfiguration).use {
-            threads.mapEventsWithEdition(it, roomId)
-        }
+    override suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) {
+        fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
+                roomId = roomId,
+                rootThreadEventId = rootThreadEventId,
+                from = from,
+                limit = limit
+        ))
     }
 
-    override suspend fun markThreadAsRead(rootThreadEventId: String) {
-        monarchy.awaitTransaction {
-            EventEntity.where(
-                    realm = it,
-                    eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
-        }
+    override suspend fun fetchThreadSummaries() {
+        fetchThreadSummariesTask.execute(FetchThreadSummariesTask.Params(
+                roomId = roomId
+        ))
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3bc36fb2a804c8b9dfc0452a1f63d5a3e774dfbf
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.threads.local
+
+import androidx.lifecycle.LiveData
+import com.zhuinden.monarchy.Monarchy
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import io.realm.Realm
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
+import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
+import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
+import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
+import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
+import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.util.awaitTransaction
+
+internal class DefaultThreadsLocalService @AssistedInject constructor(
+        @Assisted private val roomId: String,
+        @UserId private val userId: String,
+        @SessionDatabase private val monarchy: Monarchy,
+        private val timelineEventMapper: TimelineEventMapper,
+) : ThreadsLocalService {
+
+    @AssistedFactory
+    interface Factory {
+        fun create(roomId: String): DefaultThreadsLocalService
+    }
+
+    override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
+        return monarchy.findAllMappedWithChanges(
+                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun getMarkedThreadNotifications(): List<TimelineEvent> {
+        return monarchy.fetchAllMappedSync(
+                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
+        return monarchy.findAllMappedWithChanges(
+                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun getAllThreads(): List<TimelineEvent> {
+        return monarchy.fetchAllMappedSync(
+                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
+        return Realm.getInstance(monarchy.realmConfiguration).use {
+            TimelineEventEntity.isUserParticipatingInThread(
+                    realm = it,
+                    roomId = roomId,
+                    rootThreadEventId = rootThreadEventId,
+                    senderId = userId)
+        }
+    }
+
+    override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> {
+        return Realm.getInstance(monarchy.realmConfiguration).use {
+            threads.mapEventsWithEdition(it, roomId)
+        }
+    }
+
+    override suspend fun markThreadAsRead(rootThreadEventId: String) {
+        monarchy.awaitTransaction {
+            EventEntity.where(
+                    realm = it,
+                    eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 3dd4225b2c3fdee5e12c4062ff875f61d70294bf..8c2b4d2bbe2cf4327276846e52889277f7573792 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
 import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
 import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
@@ -58,6 +59,7 @@ internal class DefaultTimeline(private val roomId: String,
                                paginationTask: PaginationTask,
                                getEventTask: GetContextOfEventTask,
                                fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
+                               fetchThreadTimelineTask: FetchThreadTimelineTask,
                                timelineEventMapper: TimelineEventMapper,
                                timelineInput: TimelineInput,
                                threadsAwarenessHandler: ThreadsAwarenessHandler,
@@ -89,7 +91,9 @@ internal class DefaultTimeline(private val roomId: String,
             realm = backgroundRealm,
             eventDecryptor = eventDecryptor,
             paginationTask = paginationTask,
+            realmConfiguration = realmConfiguration,
             fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
+            fetchThreadTimelineTask = fetchThreadTimelineTask,
             getContextOfEventTask = getEventTask,
             timelineInput = timelineInput,
             timelineEventMapper = timelineEventMapper,
@@ -297,7 +301,13 @@ internal class DefaultTimeline(private val roomId: String,
         Timber.v("Post snapshot of ${snapshot.size} events")
         withContext(coroutineDispatchers.main) {
             listeners.forEach {
-                tryOrNull { it.onTimelineUpdated(snapshot) }
+                if (initialEventId != null && isFromThreadTimeline && snapshot.firstOrNull { it.eventId == initialEventId } == null) {
+                    // We are in a thread timeline with a permalink, post update timeline only when the appropriate message have been found
+                    tryOrNull { it.onTimelineUpdated(arrayListOf()) }
+                } else {
+                    // In all the other cases update timeline as expected
+                    tryOrNull { it.onTimelineUpdated(snapshot) }
+                }
             }
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
index 8094fee504ff535d9b78aa152e8e7b2c0eb5c50d..1ba2aff191ed0afacb72d24ac64b556044d43e75 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
@@ -31,6 +31,7 @@ import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsS
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
 
@@ -42,6 +43,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
         private val eventDecryptor: TimelineEventDecryptor,
         private val paginationTask: PaginationTask,
         private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
+        private val fetchThreadTimelineTask: FetchThreadTimelineTask,
         private val timelineEventMapper: TimelineEventMapper,
         private val loadRoomMembersTask: LoadRoomMembersTask,
         private val threadsAwarenessHandler: ThreadsAwarenessHandler,
@@ -64,10 +66,11 @@ internal class DefaultTimelineService @AssistedInject constructor(
                 realmConfiguration = monarchy.realmConfiguration,
                 coroutineDispatchers = coroutineDispatchers,
                 paginationTask = paginationTask,
+                fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
                 timelineEventMapper = timelineEventMapper,
                 timelineInput = timelineInput,
                 eventDecryptor = eventDecryptor,
-                fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
+                fetchThreadTimelineTask = fetchThreadTimelineTask,
                 loadRoomMembersTask = loadRoomMembersTask,
                 readReceiptHandler = readReceiptHandler,
                 getEventTask = contextOfEventTask,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index f332c4a35f608fd367b7c8d02b200ac331899ca9..a9e7b3bcdc43a293e08083a026fde7f2722308eb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -19,20 +19,28 @@ package org.matrix.android.sdk.internal.session.room.timeline
 import io.realm.OrderedCollectionChangeSet
 import io.realm.OrderedRealmCollectionChangeListener
 import io.realm.Realm
+import io.realm.RealmConfiguration
 import io.realm.RealmResults
+import io.realm.kotlin.createObject
 import kotlinx.coroutines.CompletableDeferred
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.internal.database.helper.addIfNecessary
 import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.deleteAndClearThreadEvents
 import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
 import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
+import timber.log.Timber
 import java.util.concurrent.atomic.AtomicReference
 
 /**
@@ -76,6 +84,8 @@ internal class LoadTimelineStrategy(
             val realm: AtomicReference<Realm>,
             val eventDecryptor: TimelineEventDecryptor,
             val paginationTask: PaginationTask,
+            val realmConfiguration: RealmConfiguration,
+            val fetchThreadTimelineTask: FetchThreadTimelineTask,
             val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
             val getContextOfEventTask: GetContextOfEventTask,
             val timelineInput: TimelineInput,
@@ -90,7 +100,6 @@ internal class LoadTimelineStrategy(
     private var getContextLatch: CompletableDeferred<Unit>? = null
     private var chunkEntity: RealmResults<ChunkEntity>? = null
     private var timelineChunk: TimelineChunk? = null
-
     private val chunkEntityListener = OrderedRealmCollectionChangeListener { _: RealmResults<ChunkEntity>, changeSet: OrderedCollectionChangeSet ->
         // Can be call either when you open a permalink on an unknown event
         // or when there is a gap in the timeline.
@@ -170,6 +179,9 @@ internal class LoadTimelineStrategy(
         getContextLatch?.cancel()
         chunkEntity = null
         timelineChunk = null
+        if (mode is Mode.Thread) {
+            clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId)
+        }
     }
 
     suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
@@ -185,6 +197,9 @@ internal class LoadTimelineStrategy(
                 return LoadMoreResult.FAILURE
             }
         }
+        if (mode is Mode.Thread) {
+            return timelineChunk?.loadMoreThread(count) ?: LoadMoreResult.FAILURE
+        }
         return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
     }
 
@@ -201,7 +216,7 @@ internal class LoadTimelineStrategy(
     }
 
     private fun buildSendingEvents(): List<TimelineEvent> {
-        return if (hasReachedLastForward()) {
+        return if (hasReachedLastForward() || mode is Mode.Thread) {
             sendingEventsDataSource.buildSendingEvents()
         } else {
             emptyList()
@@ -219,13 +234,47 @@ internal class LoadTimelineStrategy(
                 ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId))
             }
             is Mode.Thread    -> {
+                recreateThreadChunkEntity(realm, mode.rootThreadEventId)
                 ChunkEntity.where(realm, roomId)
-                        .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
+                        .equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, mode.rootThreadEventId)
+                        .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
                         .findAll()
             }
         }
     }
 
+    /**
+     * Clear any existing thread chunk entity and create a new one, with the
+     * rootThreadEventId included
+     */
+    private fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
+        realm.executeTransaction {
+            // Lets delete the chunk and start a new one
+            ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
+                Timber.i("###THREADS LoadTimelineStrategy [onStart] thread chunk cleared..")
+            }
+            val threadChunk = it.createObject<ChunkEntity>().apply {
+                Timber.i("###THREADS LoadTimelineStrategy [onStart] Created new thread chunk with rootThreadEventId: $rootThreadEventId")
+                this.rootThreadEventId = rootThreadEventId
+                this.isLastForwardThread = true
+            }
+            if (threadChunk.isValid) {
+                RoomEntity.where(it, roomId).findFirst()?.addIfNecessary(threadChunk)
+            }
+        }
+    }
+
+    /**
+     * Clear any existing thread chunk
+     */
+    private fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
+        realm.executeTransaction {
+            ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
+                Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..")
+            }
+        }
+    }
+
     private fun hasReachedLastForward(): Boolean {
         return timelineChunk?.hasReachedLastForward().orFalse()
     }
@@ -237,8 +286,10 @@ internal class LoadTimelineStrategy(
                     timelineSettings = dependencies.timelineSettings,
                     roomId = roomId,
                     timelineId = timelineId,
+                    fetchThreadTimelineTask = dependencies.fetchThreadTimelineTask,
                     eventDecryptor = dependencies.eventDecryptor,
                     paginationTask = dependencies.paginationTask,
+                    realmConfiguration = dependencies.realmConfiguration,
                     fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask,
                     timelineEventMapper = dependencies.timelineEventMapper,
                     uiEchoManager = uiEchoManager,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
index 1262c09d974b1667baaa1c0291e59596857a5b3c..637267a9b17c3c5ff066998a130286e43cdd9a92 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
@@ -45,9 +45,11 @@ internal class RealmSendingEventsDataSource(
     private var frozenSendingTimelineEvents: RealmList<TimelineEventEntity>? = null
 
     private val sendingTimelineEventsListener = RealmChangeListener<RealmList<TimelineEventEntity>> { events ->
-        uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
-        updateFrozenResults(events)
-        onEventsUpdated(false)
+        if (events.isValid) {
+            uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
+            updateFrozenResults(events)
+            onEventsUpdated(false)
+        }
     }
 
     override fun start() {
@@ -55,6 +57,7 @@ internal class RealmSendingEventsDataSource(
         roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst()
         sendingTimelineEvents = roomEntity?.sendingTimelineEvents
         sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener)
+        updateFrozenResults(sendingTimelineEvents)
     }
 
     override fun stop() {
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 c0dc31fcf8a988fec4c068ccdd0afc05354cc8d0..c8f2132ae66bf3e869dbedc7a74ea03892d3f3b4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
 
 import io.realm.OrderedCollectionChangeSet
 import io.realm.OrderedRealmCollectionChangeListener
+import io.realm.RealmConfiguration
 import io.realm.RealmObjectChangeListener
 import io.realm.RealmQuery
 import io.realm.RealmResults
@@ -36,6 +37,8 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
 import timber.log.Timber
 import java.util.Collections
@@ -50,8 +53,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                              private val timelineSettings: TimelineSettings,
                              private val roomId: String,
                              private val timelineId: String,
+                             private val fetchThreadTimelineTask: FetchThreadTimelineTask,
                              private val eventDecryptor: TimelineEventDecryptor,
                              private val paginationTask: PaginationTask,
+                             private val realmConfiguration: RealmConfiguration,
                              private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
                              private val timelineEventMapper: TimelineEventMapper,
                              private val uiEchoManager: UIEchoManager? = null,
@@ -78,11 +83,15 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
             isLastBackward.set(chunkEntity.isLastBackward)
         }
         if (changeSet.isFieldChanged(ChunkEntityFields.NEXT_CHUNK.`$`)) {
-            nextChunk = createTimelineChunk(chunkEntity.nextChunk)
+            nextChunk = createTimelineChunk(chunkEntity.nextChunk).also {
+                it?.prevChunk = this
+            }
             nextChunkLatch?.complete(Unit)
         }
         if (changeSet.isFieldChanged(ChunkEntityFields.PREV_CHUNK.`$`)) {
-            prevChunk = createTimelineChunk(chunkEntity.prevChunk)
+            prevChunk = createTimelineChunk(chunkEntity.prevChunk).also {
+                it?.nextChunk = this
+            }
             prevChunkLatch?.complete(Unit)
         }
     }
@@ -141,29 +150,57 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         val loadFromStorage = loadFromStorage(count, direction).also {
             logLoadedFromStorage(it, direction)
         }
+        if (loadFromStorage.numberOfEvents == 6) {
+            Timber.i("here")
+        }
 
         val offsetCount = count - loadFromStorage.numberOfEvents
 
-        return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) {
+        return if (offsetCount == 0) {
+            LoadMoreResult.SUCCESS
+        } else 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 {
             delegateLoadMore(fetchOnServerIfNeeded, offsetCount, direction)
         }
     }
 
+    /**
+     * This function will fetch more live thread timeline events using the /relations api. It will
+     * always fetch results, while we want our data to be up to dated.
+     */
+    suspend fun loadMoreThread(count: Int, direction: Timeline.Direction = Timeline.Direction.BACKWARDS): LoadMoreResult {
+        val rootThreadEventId = timelineSettings.rootThreadEventId ?: return LoadMoreResult.FAILURE
+        return if (direction == Timeline.Direction.BACKWARDS) {
+            try {
+                fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
+                        roomId,
+                        rootThreadEventId,
+                        chunkEntity.prevToken,
+                        count
+                )).toLoadMoreResult()
+            } catch (failure: Throwable) {
+                Timber.e(failure, "Failed to fetch thread timeline events from the server")
+                LoadMoreResult.FAILURE
+            }
+        } else {
+            LoadMoreResult.FAILURE
+        }
+    }
+
     private suspend fun delegateLoadMore(fetchFromServerIfNeeded: Boolean, offsetCount: Int, direction: Timeline.Direction): LoadMoreResult {
         return if (direction == Timeline.Direction.FORWARDS) {
             val nextChunkEntity = chunkEntity.nextChunk
             when {
                 nextChunkEntity != null -> {
                     if (nextChunk == null) {
-                        nextChunk = createTimelineChunk(nextChunkEntity)
+                        nextChunk = createTimelineChunk(nextChunkEntity).also {
+                            it?.prevChunk = this
+                        }
                     }
                     nextChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE
                 }
@@ -179,7 +216,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
             when {
                 prevChunkEntity != null -> {
                     if (prevChunk == null) {
-                        prevChunk = createTimelineChunk(prevChunkEntity)
+                        prevChunk = createTimelineChunk(prevChunkEntity).also {
+                            it?.nextChunk = this
+                        }
                     }
                     prevChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE
                 }
@@ -413,6 +452,14 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         }
     }
 
+    private fun DefaultFetchThreadTimelineTask.Result.toLoadMoreResult(): LoadMoreResult {
+        return when (this) {
+            DefaultFetchThreadTimelineTask.Result.REACHED_END -> LoadMoreResult.REACHED_END
+            DefaultFetchThreadTimelineTask.Result.SHOULD_FETCH_MORE,
+            DefaultFetchThreadTimelineTask.Result.SUCCESS     -> LoadMoreResult.SUCCESS
+        }
+    }
+
     private fun getOffsetIndex(): Int {
         var offset = 0
         var currentNextChunk = nextChunk
@@ -454,6 +501,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 }
             }
         }
+
         if (insertions.isNotEmpty() || modifications.isNotEmpty()) {
             onBuiltEvents(true)
         }
@@ -487,6 +535,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 timelineId = timelineId,
                 eventDecryptor = eventDecryptor,
                 paginationTask = paginationTask,
+                realmConfiguration = realmConfiguration,
+                fetchThreadTimelineTask = fetchThreadTimelineTask,
                 fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
                 timelineEventMapper = timelineEventMapper,
                 uiEchoManager = uiEchoManager,
@@ -508,13 +558,18 @@ private fun RealmQuery<TimelineEventEntity>.offsets(
         count: Int,
         startDisplayIndex: Int
 ): RealmQuery<TimelineEventEntity> {
-    sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
-    if (direction == Timeline.Direction.BACKWARDS) {
+    return if (direction == Timeline.Direction.BACKWARDS) {
         lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
+        sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
+        limit(count.toLong())
     } else {
         greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
+        // We need to sort ascending first so limit works in the right direction
+        sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
+        limit(count.toLong())
+        // Result is expected to be sorted descending
+        sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
     }
-    return limit(count.toLong())
 }
 
 private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
@@ -533,7 +588,6 @@ private fun ChunkEntity.sortedTimelineEvents(rootThreadEventId: String?): RealmR
                 .or()
                 .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId)
                 .endGroup()
-                .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
                 .findAll()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
index 49a8a8b55a33279e3a5abd98c6707b7eaf96d84b..3ddd877b78936b8332f78e6e5e509dee06fde42a 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
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
 
 import io.realm.Realm
 import io.realm.RealmConfiguration
+import kotlinx.coroutines.runBlocking
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.events.model.Event
@@ -99,7 +100,13 @@ internal class TimelineEventDecryptor @Inject constructor(
         }
         executor?.execute {
             Realm.getInstance(realmConfiguration).use { realm ->
-                processDecryptRequest(request, realm)
+                try {
+                    runBlocking {
+                        processDecryptRequest(request, realm)
+                    }
+                } catch (e: InterruptedException) {
+                    Timber.i("Decryption got interrupted")
+                }
             }
         }
     }
@@ -115,7 +122,7 @@ internal class TimelineEventDecryptor @Inject constructor(
                 threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
         }
     }
-    private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
+    private suspend fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
         val event = request.event
         val timelineId = request.timelineId
 
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 6607e71bd9cb530bc96ef00b673b7e602ebaf174..63383a99b3c961ab414590164bb124fc6c85f9e7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
@@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
 import org.matrix.android.sdk.internal.database.query.create
 import org.matrix.android.sdk.internal.database.query.find
@@ -49,10 +50,10 @@ import javax.inject.Inject
  * Insert Chunk in DB, and eventually link next and previous chunk in db.
  */
 internal class TokenChunkEventPersistor @Inject constructor(
-                                                            @SessionDatabase private val monarchy: Monarchy,
-                                                            @UserId private val userId: String,
-                                                            private val lightweightSettingsStorage: LightweightSettingsStorage,
-                                                            private val liveEventManager: Lazy<StreamEventsManager>) {
+        @SessionDatabase private val monarchy: Monarchy,
+        @UserId private val userId: String,
+        private val lightweightSettingsStorage: LightweightSettingsStorage,
+        private val liveEventManager: Lazy<StreamEventsManager>) {
 
     enum class Result {
         SHOULD_FETCH_MORE,
@@ -145,9 +146,12 @@ internal class TokenChunkEventPersistor @Inject constructor(
                 if (event.eventId == null || event.senderId == null) {
                     return@forEach
                 }
-                // We check for the timeline event with this id
+                // We check for the timeline event with this id, but not in the thread chunk
                 val eventId = event.eventId
-                val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
+                val existingTimelineEvent = TimelineEventEntity
+                        .where(realm, roomId, eventId)
+                        .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
+                        .findFirst()
                 // If it exists, we want to stop here, just link the prevChunk
                 val existingChunk = existingTimelineEvent?.chunk?.firstOrNull()
                 if (existingChunk != null) {
@@ -173,7 +177,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
                     return@processTimelineEvents
                 }
                 val ageLocalTs = event.unsignedData?.age?.let { now - it }
-                val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+                var eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
                 if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
                     val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
                         event.prevContent
@@ -183,7 +187,11 @@ internal class TokenChunkEventPersistor @Inject constructor(
                     roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
                 }
                 liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
-                currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
+                currentChunk.addTimelineEvent(
+                        roomId = roomId,
+                        eventEntity = eventEntity,
+                        direction = direction,
+                        roomMemberContentsByUser = roomMemberContentsByUser)
                 if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
                     eventEntity.rootThreadEventId?.let {
                         // This is a thread event
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
index c18055e0894f9f8f3e44ac64d2175347cd7a47b3..e764ab551ae90f7bca5a5ff69a3f863f523bb19f 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
@@ -113,71 +113,108 @@ internal class DefaultSpaceService @Inject constructor(
         return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId))
     }
 
-    override suspend fun querySpaceChildren(spaceId: String,
-                                            suggestedOnly: Boolean?,
-                                            limit: Int?,
-                                            from: String?,
-                                            knownStateList: List<Event>?): SpaceHierarchyData {
-        return resolveSpaceInfoTask.execute(
-                ResolveSpaceInfoTask.Params(
-                        spaceId = spaceId, limit = limit, maxDepth = 1, from = from, suggestedOnly = suggestedOnly
-                )
-        ).let { response ->
-            val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId }
-            val root = RoomSummary(
-                    roomId = spaceDesc?.roomId ?: spaceId,
-                    roomType = spaceDesc?.roomType,
-                    name = spaceDesc?.name ?: "",
-                    displayName = spaceDesc?.name ?: "",
-                    topic = spaceDesc?.topic ?: "",
-                    joinedMembersCount = spaceDesc?.numJoinedMembers,
-                    avatarUrl = spaceDesc?.avatarUrl ?: "",
-                    encryptionEventTs = null,
-                    typingUsers = emptyList(),
-                    isEncrypted = false,
-                    flattenParentIds = emptyList(),
-                    canonicalAlias = spaceDesc?.canonicalAlias,
-                    joinRules = RoomJoinRules.PUBLIC.takeIf { spaceDesc?.worldReadable == true }
-            )
-            val children = response.rooms
-                    ?.filter { it.roomId != spaceId }
-                    ?.flatMap { childSummary ->
-                        (spaceDesc?.childrenState ?: knownStateList)
-                                ?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
-                                ?.mapNotNull { childStateEv ->
-                                    // create a child entry for everytime this room is the child of a space
-                                    // beware that a room could appear then twice in this list
-                                    childStateEv.content.toModel<SpaceChildContent>()?.let { childStateEvContent ->
-                                        SpaceChildInfo(
-                                                childRoomId = childSummary.roomId,
-                                                isKnown = true,
-                                                roomType = childSummary.roomType,
-                                                name = childSummary.name,
-                                                topic = childSummary.topic,
-                                                avatarUrl = childSummary.avatarUrl,
-                                                order = childStateEvContent.order,
-//                                                        autoJoin = childStateEvContent.autoJoin ?: false,
-                                                viaServers = childStateEvContent.via.orEmpty(),
-                                                activeMemberCount = childSummary.numJoinedMembers,
-                                                parentRoomId = childStateEv.roomId,
-                                                suggested = childStateEvContent.suggested,
-                                                canonicalAlias = childSummary.canonicalAlias,
-                                                aliases = childSummary.aliases,
-                                                worldReadable = childSummary.worldReadable
-                                        )
-                                    }
-                                }.orEmpty()
-                    }
-                    .orEmpty()
-            SpaceHierarchyData(
-                    rootSummary = root,
-                    children = children,
-                    childrenState = spaceDesc?.childrenState.orEmpty(),
-                    nextToken = response.nextBatch
+    override suspend fun querySpaceChildren(
+            spaceId: String,
+            suggestedOnly: Boolean?,
+            limit: Int?,
+            from: String?,
+            knownStateList: List<Event>?
+    ): SpaceHierarchyData {
+        val spacesResponse = getSpacesResponse(spaceId, suggestedOnly, limit, from)
+        val spaceRootResponse = spacesResponse.getRoot(spaceId)
+        val spaceRoot = spaceRootResponse?.toRoomSummary() ?: createBlankRoomSummary(spaceId)
+        val spaceChildren = spacesResponse.rooms.mapSpaceChildren(spaceId, spaceRootResponse, knownStateList)
+
+        return SpaceHierarchyData(
+                rootSummary = spaceRoot,
+                children = spaceChildren,
+                childrenState = spaceRootResponse?.childrenState.orEmpty(),
+                nextToken = spacesResponse.nextBatch
+        )
+    }
+
+    private suspend fun getSpacesResponse(spaceId: String, suggestedOnly: Boolean?, limit: Int?, from: String?) =
+            resolveSpaceInfoTask.execute(
+                    ResolveSpaceInfoTask.Params(spaceId = spaceId, limit = limit, maxDepth = 1, from = from, suggestedOnly = suggestedOnly)
             )
-        }
+
+    private fun SpacesResponse.getRoot(spaceId: String) = rooms?.firstOrNull { it.roomId == spaceId }
+
+    private fun SpaceChildSummaryResponse.toRoomSummary() = RoomSummary(
+            roomId = roomId,
+            roomType = roomType,
+            name = name ?: "",
+            displayName = name ?: "",
+            topic = topic ?: "",
+            joinedMembersCount = numJoinedMembers,
+            avatarUrl = avatarUrl ?: "",
+            encryptionEventTs = null,
+            typingUsers = emptyList(),
+            isEncrypted = false,
+            flattenParentIds = emptyList(),
+            canonicalAlias = canonicalAlias,
+            joinRules = RoomJoinRules.PUBLIC.takeIf { isWorldReadable }
+    )
+
+    private fun createBlankRoomSummary(spaceId: String) = RoomSummary(
+            roomId = spaceId,
+            joinedMembersCount = null,
+            encryptionEventTs = null,
+            typingUsers = emptyList(),
+            isEncrypted = false,
+            flattenParentIds = emptyList(),
+            canonicalAlias = null,
+            joinRules = null
+    )
+
+    private fun List<SpaceChildSummaryResponse>?.mapSpaceChildren(
+            spaceId: String,
+            spaceRootResponse: SpaceChildSummaryResponse?,
+            knownStateList: List<Event>?,
+    ) = this?.filterIdIsNot(spaceId)
+            ?.toSpaceChildInfoList(spaceId, spaceRootResponse, knownStateList)
+            .orEmpty()
+
+    private fun List<SpaceChildSummaryResponse>.filterIdIsNot(spaceId: String) = filter { it.roomId != spaceId }
+
+    private fun List<SpaceChildSummaryResponse>.toSpaceChildInfoList(
+            spaceId: String,
+            rootRoomResponse: SpaceChildSummaryResponse?,
+            knownStateList: List<Event>?,
+    ) = flatMap { spaceChildSummary ->
+        (rootRoomResponse?.childrenState ?: knownStateList)
+                ?.filter { it.isChildOf(spaceChildSummary) }
+                ?.mapNotNull { childStateEvent -> childStateEvent.toSpaceChildInfo(spaceId, spaceChildSummary) }
+                .orEmpty()
     }
 
+    private fun Event.isChildOf(space: SpaceChildSummaryResponse) = stateKey == space.roomId && type == EventType.STATE_SPACE_CHILD
+
+    private fun Event.toSpaceChildInfo(spaceId: String, summary: SpaceChildSummaryResponse) = content.toModel<SpaceChildContent>()?.let { content ->
+        createSpaceChildInfo(spaceId, summary, content)
+    }
+
+    private fun createSpaceChildInfo(
+            spaceId: String,
+            summary: SpaceChildSummaryResponse,
+            content: SpaceChildContent
+    ) = SpaceChildInfo(
+            childRoomId = summary.roomId,
+            isKnown = true,
+            roomType = summary.roomType,
+            name = summary.name,
+            topic = summary.topic,
+            avatarUrl = summary.avatarUrl,
+            order = content.order,
+            viaServers = content.via.orEmpty(),
+            activeMemberCount = summary.numJoinedMembers,
+            parentRoomId = spaceId,
+            suggested = content.suggested,
+            canonicalAlias = summary.canonicalAlias,
+            aliases = summary.aliases,
+            worldReadable = summary.isWorldReadable
+    )
+
     override suspend fun joinSpace(spaceIdOrAlias: String,
                                    reason: String?,
                                    viaServers: List<String>): JoinSpaceResult {
@@ -192,10 +229,6 @@ internal class DefaultSpaceService @Inject constructor(
         leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason))
     }
 
-//    override fun getSpaceParentsOfRoom(roomId: String): List<SpaceSummary> {
-//        return spaceSummaryDataSource.getParentsOfRoom(roomId)
-//    }
-
     override suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List<String>) {
         // Should we perform some validation here?,
         // and if client want to bypass, it could use sendStateEvent directly?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt
index 2a396d6ee7b444f1435a6751d784b9686021a2e1..d59ca06c2cda6fdf852c47cedb54d23bc6c81c1d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.space
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
+import retrofit2.HttpException
 import javax.inject.Inject
 
 internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, SpacesResponse> {
@@ -28,7 +29,6 @@ internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, Spac
             val maxDepth: Int?,
             val from: String?,
             val suggestedOnly: Boolean?
-//            val autoJoinOnly: Boolean?
     )
 }
 
@@ -36,14 +36,30 @@ internal class DefaultResolveSpaceInfoTask @Inject constructor(
         private val spaceApi: SpaceApi,
         private val globalErrorReceiver: GlobalErrorReceiver
 ) : ResolveSpaceInfoTask {
-    override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse {
-        return executeRequest(globalErrorReceiver) {
+
+    override suspend fun execute(params: ResolveSpaceInfoTask.Params) = executeRequest(globalErrorReceiver) {
+        try {
+            getSpaceHierarchy(params)
+        } catch (e: HttpException) {
+            getUnstableSpaceHierarchy(params)
+        }
+    }
+
+    private suspend fun getSpaceHierarchy(params: ResolveSpaceInfoTask.Params) =
             spaceApi.getSpaceHierarchy(
                     spaceId = params.spaceId,
                     suggestedOnly = params.suggestedOnly,
                     limit = params.limit,
                     maxDepth = params.maxDepth,
-                    from = params.from)
-        }
-    }
+                    from = params.from,
+            )
+
+    private suspend fun getUnstableSpaceHierarchy(params: ResolveSpaceInfoTask.Params) =
+            spaceApi.getSpaceHierarchyUnstable(
+                    spaceId = params.spaceId,
+                    suggestedOnly = params.suggestedOnly,
+                    limit = params.limit,
+                    maxDepth = params.maxDepth,
+                    from = params.from,
+            )
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt
index edd10bc2ef9040c2e4e91cd10a7466d5735ad900..fda9b4b5bc6db6418bb8e54b5d7242cd3452d4bc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt
@@ -31,11 +31,22 @@ internal interface SpaceApi {
      * @param from: Optional. Pagination token given to retrieve the next set of rooms.
      * Note that if a pagination token is provided, then the parameters given for suggested_only and max_depth must be the same.
      */
-    @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/hierarchy")
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomId}/hierarchy")
     suspend fun getSpaceHierarchy(
             @Path("roomId") spaceId: String,
             @Query("suggested_only") suggestedOnly: Boolean?,
             @Query("limit") limit: Int?,
             @Query("max_depth") maxDepth: Int?,
             @Query("from") from: String?): SpacesResponse
+
+    /**
+     * Unstable version of [getSpaceHierarchy]
+     */
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/hierarchy")
+    suspend fun getSpaceHierarchyUnstable(
+            @Path("roomId") spaceId: String,
+            @Query("suggested_only") suggestedOnly: Boolean?,
+            @Query("limit") limit: Int?,
+            @Query("max_depth") maxDepth: Int?,
+            @Query("from") from: String?): SpacesResponse
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt
index e0f273d0c2510c8b838e4d11d3bbbfc686bd4c72..b6a9c73d36596323af4f8855b7ac259e4fbad87c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt
@@ -81,7 +81,7 @@ internal data class SpaceChildSummaryResponse(
          * Required. Whether the room may be viewed by guest users without joining.
          */
         @Json(name = "world_readable")
-        val worldReadable: Boolean = false,
+        val isWorldReadable: Boolean = false,
 
         /**
          * Required. Whether guest users may join the room and participate in it. If they can,
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 f93da9705d0d58b91c2c893b150bb351f1b250b5..1bbf54a7888942fe298bc584fd1d53f9ecbc2761 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
@@ -110,6 +110,7 @@ internal class SyncResponseHandler @Inject constructor(
 
         // Start one big transaction
         monarchy.awaitTransaction { realm ->
+            // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local)
             measureTimeMillis {
                 Timber.v("Handle rooms")
                 reportSubtask(reporter, InitSyncStep.ImportingAccountRoom, 1, 0.7f) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
index b4da1a02cd918066ac3d32bcb73b18da45ad9bba..2136259f22a4f5bfb96ed91833b119fd4f1324b8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.sync
 
 import android.os.SystemClock
 import okhttp3.ResponseBody
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.initsync.InitSyncStep
@@ -104,7 +105,11 @@ internal class DefaultSyncTask @Inject constructor(
         val isInitialSync = token == null
         if (isInitialSync) {
             // We might want to get the user information in parallel too
-            userStore.createOrUpdate(userId)
+            val user = tryOrNull { session.getProfileAsUser(userId) }
+            userStore.createOrUpdate(
+                    userId = userId,
+                    displayName = user?.displayName,
+                    avatarUrl = user?.avatarUrl)
             defaultSyncStatusService.startRoot(InitSyncStep.ImportingAccount, 100)
         }
         // Maybe refresh the homeserver capabilities data we know
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
index f299d3effa1b7b458fc2e57f7adfc599ab26e587..9ae7b827777a1aa15515bb3664871f6363cfa20b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
@@ -38,7 +38,7 @@ private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
 internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService,
                                                      private val verificationService: DefaultVerificationService) {
 
-    fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) {
+    suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) {
         val total = toDevice.events?.size ?: 0
         toDevice.events?.forEachIndexed { index, event ->
             progressReporter?.reportProgress(index * 100F / total)
@@ -66,7 +66,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
      * @param timelineId the timeline identifier
      * @return true if the event has been decrypted
      */
-    private fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean {
+    private suspend fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean {
         Timber.v("## CRYPTO | decryptToDeviceEvent")
         if (event.getClearType() == EventType.ENCRYPTED) {
             var result: MXEventDecryptionResult? = null
@@ -80,6 +80,8 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
                     it.identityKey() == senderKey
                 }?.deviceId ?: senderKey
                 Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>")
+            } catch (failure: Throwable) {
+                Timber.e(failure, "## CRYPTO | Failed to decrypt to device event from ${event.senderId}")
             }
 
             if (null != result) {
@@ -91,7 +93,9 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
                 )
                 return true
             } else {
-                // should not happen
+                // Could happen for to device events
+                // None of the known session could decrypt the message
+                // In this case unwedging process might have been started (rate limited)
                 Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}")
             }
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
index fe173a35c3855f2993006f091cbd70174cc6281c..e5bed1218197e0d6f99416d52bb519a57dd5a43f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.session.sync.handler
 
 import io.realm.Realm
+import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.getPresenceContent
 import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse
@@ -27,27 +28,29 @@ import org.matrix.android.sdk.internal.database.query.updateDirectUserPresence
 import org.matrix.android.sdk.internal.database.query.updateUserPresence
 import javax.inject.Inject
 
-internal class PresenceSyncHandler @Inject constructor() {
+internal class PresenceSyncHandler @Inject constructor(private val matrixConfiguration: MatrixConfiguration) {
 
     fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) {
-        presenceSyncResponse?.events
-                ?.filter { event -> event.type == EventType.PRESENCE }
-                ?.forEach { event ->
-                    val content = event.getPresenceContent() ?: return@forEach
-                    val userId = event.senderId ?: return@forEach
-                    val userPresenceEntity = UserPresenceEntity(
-                            userId = userId,
-                            lastActiveAgo = content.lastActiveAgo,
-                            statusMessage = content.statusMessage,
-                            isCurrentlyActive = content.isCurrentlyActive,
-                            avatarUrl = content.avatarUrl,
-                            displayName = content.displayName
-                    ).also {
-                        it.presence = content.presence
-                    }
+        if (matrixConfiguration.presenceSyncEnabled) {
+            presenceSyncResponse?.events
+                    ?.filter { event -> event.type == EventType.PRESENCE }
+                    ?.forEach { event ->
+                        val content = event.getPresenceContent() ?: return@forEach
+                        val userId = event.senderId ?: return@forEach
+                        val userPresenceEntity = UserPresenceEntity(
+                                userId = userId,
+                                lastActiveAgo = content.lastActiveAgo,
+                                statusMessage = content.statusMessage,
+                                isCurrentlyActive = content.isCurrentlyActive,
+                                avatarUrl = content.avatarUrl,
+                                displayName = content.displayName
+                        ).also {
+                            it.presence = content.presence
+                        }
 
-                    storePresenceToDB(realm, userPresenceEntity)
-                }
+                        storePresenceToDB(realm, userPresenceEntity)
+                    }
+        }
     }
 
     /**
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 99e6521eb70b76656b477d0cedc15055b98d9d6d..8fe85f0d318aed6548320cb36b7cb8f240cda583 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
@@ -19,14 +19,17 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
 import dagger.Lazy
 import io.realm.Realm
 import io.realm.kotlin.createObject
+import kotlinx.coroutines.runBlocking
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.api.session.initsync.InitSyncStep
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
 import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync
 import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral
 import org.matrix.android.sdk.api.session.sync.model.RoomSync
@@ -36,6 +39,7 @@ import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.database.helper.addIfNecessary
 import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
+import org.matrix.android.sdk.internal.database.helper.createOrUpdate
 import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
 import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.asDomain
@@ -46,10 +50,13 @@ import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.model.deleteOnCascade
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
 import org.matrix.android.sdk.internal.database.query.find
 import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.getOrNull
 import org.matrix.android.sdk.internal.database.query.where
@@ -84,6 +91,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val threadsAwarenessHandler: ThreadsAwarenessHandler,
                                                    private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
                                                    @UserId private val userId: String,
+                                                   private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
                                                    private val lightweightSettingsStorage: LightweightSettingsStorage,
                                                    private val timelineInput: TimelineInput,
                                                    private val liveEventService: Lazy<StreamEventsManager>) {
@@ -94,11 +102,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
     }
 
-    fun handle(realm: Realm,
-               roomsSyncResponse: RoomsSyncResponse,
-               isInitialSync: Boolean,
-               aggregator: SyncResponsePostTreatmentAggregator,
-               reporter: ProgressReporter? = null) {
+    suspend fun handle(realm: Realm,
+                       roomsSyncResponse: RoomsSyncResponse,
+                       isInitialSync: Boolean,
+                       aggregator: SyncResponsePostTreatmentAggregator,
+                       reporter: ProgressReporter? = null) {
         Timber.v("Execute transaction from $this")
         handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter)
         handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter)
@@ -113,11 +121,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
     }
     // PRIVATE METHODS *****************************************************************************
 
-    private fun handleRoomSync(realm: Realm,
-                               handlingStrategy: HandlingStrategy,
-                               isInitialSync: Boolean,
-                               aggregator: SyncResponsePostTreatmentAggregator,
-                               reporter: ProgressReporter?) {
+    private suspend fun handleRoomSync(realm: Realm,
+                                       handlingStrategy: HandlingStrategy,
+                                       isInitialSync: Boolean,
+                                       aggregator: SyncResponsePostTreatmentAggregator,
+                                       reporter: ProgressReporter?) {
         val insertType = if (isInitialSync) {
             EventInsertType.INITIAL_SYNC
         } else {
@@ -150,11 +158,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         realm.insertOrUpdate(rooms)
     }
 
-    private fun insertJoinRoomsFromInitSync(realm: Realm,
-                                            handlingStrategy: HandlingStrategy.JOINED,
-                                            syncLocalTimeStampMillis: Long,
-                                            aggregator: SyncResponsePostTreatmentAggregator,
-                                            reporter: ProgressReporter?) {
+    private suspend fun insertJoinRoomsFromInitSync(realm: Realm,
+                                                    handlingStrategy: HandlingStrategy.JOINED,
+                                                    syncLocalTimeStampMillis: Long,
+                                                    aggregator: SyncResponsePostTreatmentAggregator,
+                                                    reporter: ProgressReporter?) {
         val bestChunkSize = computeBestChunkSize(
                 listSize = handlingStrategy.data.keys.size,
                 limit = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE
@@ -192,12 +200,12 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         }
     }
 
-    private fun handleJoinedRoom(realm: Realm,
-                                 roomId: String,
-                                 roomSync: RoomSync,
-                                 insertType: EventInsertType,
-                                 syncLocalTimestampMillis: Long,
-                                 aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
+    private suspend fun handleJoinedRoom(realm: Realm,
+                                         roomId: String,
+                                         roomSync: RoomSync,
+                                         insertType: EventInsertType,
+                                         syncLocalTimestampMillis: Long,
+                                         aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
         Timber.v("Handle join sync for room $roomId")
 
         val ephemeralResult = (roomSync.ephemeral as? LazyRoomSyncEphemeral.Parsed)
@@ -343,15 +351,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         return roomEntity
     }
 
-    private fun handleTimelineEvents(realm: Realm,
-                                     roomId: String,
-                                     roomEntity: RoomEntity,
-                                     eventList: List<Event>,
-                                     prevToken: String? = null,
-                                     isLimited: Boolean = true,
-                                     insertType: EventInsertType,
-                                     syncLocalTimestampMillis: Long,
-                                     aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
+    private suspend fun handleTimelineEvents(realm: Realm,
+                                             roomId: String,
+                                             roomEntity: RoomEntity,
+                                             eventList: List<Event>,
+                                             prevToken: String? = null,
+                                             isLimited: Boolean = true,
+                                             insertType: EventInsertType,
+                                             syncLocalTimestampMillis: Long,
+                                             aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
         val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
         if (isLimited && lastChunk != null) {
             lastChunk.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true)
@@ -379,7 +387,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
             val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
 
             if (event.isEncrypted() && !isInitialSync) {
-                decryptIfNeeded(event, roomId)
+                runBlocking {
+                    decryptIfNeeded(event, roomId)
+                }
             }
             var contentToInject: String? = null
             if (!isInitialSync) {
@@ -406,11 +416,28 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
             }
 
-            chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
+            val timelineEventAdded = chunkEntity.addTimelineEvent(
+                    roomId = roomId,
+                    eventEntity = eventEntity,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = roomMemberContentsByUser)
             if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
                 eventEntity.rootThreadEventId?.let {
                     // This is a thread event
                     optimizedThreadSummaryMap[it] = eventEntity
+                    // Add the same thread timeline event to Thread Chunk
+                    addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity)
+                    if (homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreading) {
+                        // Update thread summaries only if homeserver supports threading
+                        ThreadSummaryEntity.createOrUpdate(
+                                threadSummaryType = ThreadSummaryUpdateType.ADD,
+                                realm = realm,
+                                roomId = roomId,
+                                threadEventEntity = eventEntity,
+                                roomMemberContentsByUser = roomMemberContentsByUser,
+                                userId = userId,
+                                roomEntity = roomEntity)
+                    }
                 } ?: run {
                     // This is a normal event or a root thread one
                     optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
@@ -455,7 +482,29 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         return chunkEntity
     }
 
-    private fun decryptIfNeeded(event: Event, roomId: String) {
+    /**
+     * Adds new event to the appropriate thread chunk. If the event is already in
+     * the thread timeline and /relations api, we should not added it
+     */
+    private fun addToThreadChunkIfNeeded(realm: Realm,
+                                         roomId: String,
+                                         threadId: String,
+                                         timelineEventEntity: TimelineEventEntity?,
+                                         roomEntity: RoomEntity) {
+        val eventId = timelineEventEntity?.eventId ?: return
+
+        ChunkEntity.findLastForwardChunkOfThread(realm, roomId, threadId)?.let { threadChunk ->
+            val existingEvent = threadChunk.timelineEvents.find(eventId)
+            if (existingEvent?.ownedByThreadChunk == true) {
+                Timber.i("###THREADS RoomSyncHandler event:${timelineEventEntity.eventId} already exists, do not add")
+                return@addToThreadChunkIfNeeded
+            }
+            threadChunk.timelineEvents.add(0, timelineEventEntity)
+            roomEntity.addIfNecessary(threadChunk)
+        }
+    }
+
+    private suspend 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), "")
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 f3a1523955383185046dcae80ec8d93d7f635440..db9799d51eb0b8648df0c79f187c78af11c32869 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
 import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
+import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
 import org.matrix.android.sdk.api.util.JsonDict
@@ -161,7 +162,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                              eventEntity: EventEntity? = null): String? {
         event ?: return null
         roomId ?: return null
-        if (lightweightSettingsStorage.areThreadMessagesEnabled()) return null
+        if (lightweightSettingsStorage.areThreadMessagesEnabled() && !isReplyEvent(event)) return null
         handleRootThreadEventsIfNeeded(realm, roomId, eventEntity, event)
         if (!isThreadEvent(event)) return null
         val eventPayload = if (!event.isEncrypted()) {
@@ -170,8 +171,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
             event.mxDecryptionResult?.payload?.toMutableMap() ?: return null
         }
         val eventBody = event.getDecryptedTextSummary() ?: return null
+        val threadRelation = getRootThreadRelationContent(event)
         val eventIdToInject = getPreviousEventOrRoot(event) ?: run {
-            return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
+            return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation)
         }
         val eventToInject = getEventFromDB(realm, eventIdToInject)
         val eventToInjectBody = eventToInject?.getDecryptedTextSummary()
@@ -183,17 +185,19 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                     roomId = roomId,
                     eventBody = eventBody,
                     eventToInject = eventToInject,
-                    eventToInjectBody = eventToInjectBody) ?: return null
+                    eventToInjectBody = eventToInjectBody,
+                    threadRelation = threadRelation) ?: return null
+
             // update the event
             contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
         } else {
-            contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
+            contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation)
         }
 
         // Now lets try to find relations for improved results, while some events may come with reverse order
         eventEntity?.let {
             // When eventEntity is not null means that we are not from within roomSyncHandler
-            handleEventsThatRelatesTo(realm, roomId, event, eventBody, false)
+            handleEventsThatRelatesTo(realm, roomId, event, eventBody, false, threadRelation)
         }
         return contentForNonEncrypted
     }
@@ -205,11 +209,16 @@ internal class ThreadsAwarenessHandler @Inject constructor(
      * @param event the current event received
      * @return The content to inject in the roomSyncHandler live events
      */
-    private fun handleRootThreadEventsIfNeeded(realm: Realm, roomId: String, eventEntity: EventEntity?, event: Event): String? {
+    private fun handleRootThreadEventsIfNeeded(
+            realm: Realm,
+            roomId: String,
+            eventEntity: EventEntity?,
+            event: Event
+    ): String? {
         if (!isThreadEvent(event) && cacheEventRootId.contains(eventEntity?.eventId)) {
             eventEntity?.let {
                 val eventBody = event.getDecryptedTextSummary() ?: return null
-                return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true)
+                return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true, null)
             }
         }
         return null
@@ -224,7 +233,14 @@ internal class ThreadsAwarenessHandler @Inject constructor(
      * @param isFromCache determines whether or not we already know this is root thread event
      * @return The content to inject in the roomSyncHandler live events
      */
-    private fun handleEventsThatRelatesTo(realm: Realm, roomId: String, event: Event, eventBody: String, isFromCache: Boolean): String? {
+    private fun handleEventsThatRelatesTo(
+            realm: Realm,
+            roomId: String,
+            event: Event,
+            eventBody: String,
+            isFromCache: Boolean,
+            threadRelation: RelationDefaultContent?
+    ): String? {
         event.eventId ?: return null
         val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null
         eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound ->
@@ -236,7 +252,8 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                     roomId = roomId,
                     eventBody = newEventBody,
                     eventToInject = event,
-                    eventToInjectBody = eventBody) ?: return null
+                    eventToInjectBody = eventBody,
+                    threadRelation = threadRelation) ?: return null
 
             return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent)
         }
@@ -280,7 +297,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
     private fun injectEvent(roomId: String,
                             eventBody: String,
                             eventToInject: Event,
-                            eventToInjectBody: String): Content? {
+                            eventToInjectBody: String,
+                            threadRelation: RelationDefaultContent?
+    ): Content? {
         val eventToInjectId = eventToInject.eventId ?: return null
         val eventIdToInjectSenderId = eventToInject.senderId.orEmpty()
         val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false)
@@ -293,6 +312,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                 eventBody)
 
         return MessageTextContent(
+                relatesTo = threadRelation,
                 msgType = MessageType.MSGTYPE_TEXT,
                 format = MessageFormat.FORMAT_MATRIX_HTML,
                 body = eventBody,
@@ -306,12 +326,14 @@ internal class ThreadsAwarenessHandler @Inject constructor(
     private fun injectFallbackIndicator(event: Event,
                                         eventBody: String,
                                         eventEntity: EventEntity?,
-                                        eventPayload: MutableMap<String, Any>): String? {
+                                        eventPayload: MutableMap<String, Any>,
+                                        threadRelation: RelationDefaultContent?): String? {
         val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format(
                 "In reply to a thread",
                 eventBody)
 
         val messageTextContent = MessageTextContent(
+                relatesTo = threadRelation,
                 msgType = MessageType.MSGTYPE_TEXT,
                 format = MessageFormat.FORMAT_MATRIX_HTML,
                 body = eventBody,
@@ -332,7 +354,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                 .findAll()
         cacheEventRootId.add(rootThreadEventId)
         return threadList.filter {
-            it.asDomain().getRelationContentForType(RelationType.IO_THREAD)?.inReplyTo?.eventId == currentEventId
+            it.asDomain().getRelationContentForType(RelationType.THREAD)?.inReplyTo?.eventId == currentEventId
         }
     }
 
@@ -350,7 +372,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
      * @param event
      */
     private fun isThreadEvent(event: Event): Boolean =
-            event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.IO_THREAD
+            event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.THREAD
 
     /**
      * Returns the root thread eventId or null otherwise
@@ -359,9 +381,22 @@ internal class ThreadsAwarenessHandler @Inject constructor(
     private fun getRootThreadEventId(event: Event): String? =
             event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
 
+    private fun getRootThreadRelationContent(event: Event): RelationDefaultContent? =
+            event.content.toModel<MessageRelationContent>()?.relatesTo
+
     private fun getPreviousEventOrRoot(event: Event): String? =
             event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId
 
+    /**
+     * Returns if we should html inject the current event.
+     */
+    private fun isReplyEvent(event: Event): Boolean {
+        return isThreadEvent(event) && !isFallingBack(event) && getPreviousEventOrRoot(event) != null
+    }
+
+    private fun isFallingBack(event: Event): Boolean =
+            event.content.toModel<MessageRelationContent>()?.relatesTo?.isFallingBack == true
+
     @Suppress("UNCHECKED_CAST")
     private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
         val content = payload?.get("content") as? JsonDict
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt
index c7b125b5d6d1b245dc77c19bf9fa1f0433410576..c4fbdc75ab0d14018417dd6d9542948fbd38fcea 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt
@@ -24,8 +24,9 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.session.sync.model.accountdata.DirectMessagesContent
 import javax.inject.Inject
 
-internal class DirectChatsHelper @Inject constructor(@SessionDatabase
-                                                     private val realmConfiguration: RealmConfiguration) {
+internal class DirectChatsHelper @Inject constructor(
+        @SessionDatabase private val realmConfiguration: RealmConfiguration
+) {
 
     /**
      * @return a map of userId <-> list of roomId
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt
index 07f7c7cb868f08a56b4b8ace5fdc4f06eef0f316..1fa5e5f77120a2721666f913313750c977aeb59b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt
@@ -88,10 +88,10 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
     }
 
     /*
-   * *********************************************************************************************
-   * Message sending methods
-   * *********************************************************************************************
-   */
+     * *********************************************************************************************
+     * Message sending methods
+     * *********************************************************************************************
+     */
 
     /**
      * Send a boolean response
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
index 96655b849dffb144c59d0e15768e6e16b4caa834..088e16095031cd085e492900a755ae4647fffffe 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data
 import org.amshove.kluent.shouldBe
 import org.junit.Test
 import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
 import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk
 
 class VersionsKtTest {
@@ -53,5 +54,20 @@ class VersionsKtTest {
         Versions(supportedVersions = listOf("r0.5.0", "r0.6.0")).isSupportedBySdk() shouldBe true
         Versions(supportedVersions = listOf("r0.5.0", "r0.6.1")).isSupportedBySdk() shouldBe true
         Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true
+        Versions(supportedVersions = listOf("v1.6.0")).isSupportedBySdk() shouldBe true
+    }
+
+    @Test
+    fun doesServerSupportThreads() {
+        Versions(supportedVersions = listOf("r0.6.0")).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("r0.9.1")).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("v1.2.0")).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("v1.3.0")).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("v1.3.1")).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("v1.5.1")).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("v1.2.1"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe true
     }
 }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
index c8be0f54871b23e29b0fdd0ce95c89157ea43d9d..31fd86fe65ecf172a167ac2febfb193973c511ae 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
@@ -16,7 +16,8 @@
 
 package org.matrix.android.sdk.internal.session.pushers
 
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
 import org.amshove.kluent.internal.assertFailsWith
 import org.amshove.kluent.shouldBeEqualTo
 import org.junit.Test
@@ -39,6 +40,7 @@ private val A_JSON_PUSHER = JsonPusher(
         data = JsonPusherData(brand = "Element")
 )
 
+@ExperimentalCoroutinesApi
 class DefaultAddPusherTaskTest {
 
     private val pushersAPI = FakePushersAPI()
@@ -55,7 +57,7 @@ class DefaultAddPusherTaskTest {
     fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() {
         monarchy.givenWhereReturns<PusherEntity>(result = null)
 
-        runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+        runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
 
         pushersAPI.verifySetPusher(A_JSON_PUSHER)
         monarchy.verifyInsertOrUpdate<PusherEntity> {
@@ -70,7 +72,7 @@ class DefaultAddPusherTaskTest {
         val realmResult = PusherEntity(appDisplayName = null)
         monarchy.givenWhereReturns(result = realmResult)
 
-        runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+        runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
 
         pushersAPI.verifySetPusher(A_JSON_PUSHER)
 
@@ -85,7 +87,7 @@ class DefaultAddPusherTaskTest {
         pushersAPI.givenSetPusherErrors(SocketException())
 
         assertFailsWith<SocketException> {
-            runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+            runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
         }
 
         realmResult.state shouldBeEqualTo PusherState.FAILED_TO_REGISTER
@@ -97,7 +99,7 @@ class DefaultAddPusherTaskTest {
         pushersAPI.givenSetPusherErrors(SocketException())
 
         assertFailsWith<SocketException> {
-            runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+            runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
         }
     }
 }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7203f89629049dd2e97195163c9f03ced3565885
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.space
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import okhttp3.ResponseBody.Companion.toResponseBody
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
+import org.matrix.android.sdk.test.fakes.FakeSpaceApi
+import org.matrix.android.sdk.test.fixtures.SpacesResponseFixture.aSpacesResponse
+import retrofit2.HttpException
+import retrofit2.Response
+
+@ExperimentalCoroutinesApi
+internal class DefaultResolveSpaceInfoTaskTest {
+
+    private val spaceApi = FakeSpaceApi()
+    private val globalErrorReceiver = FakeGlobalErrorReceiver()
+    private val resolveSpaceInfoTask = DefaultResolveSpaceInfoTask(spaceApi.instance, globalErrorReceiver)
+
+    @Test
+    fun `given stable endpoint works, when execute, then return stable api data`() = runTest {
+        spaceApi.givenStableEndpointReturns(response)
+
+        val result = resolveSpaceInfoTask.execute(spaceApi.params)
+
+        result shouldBeEqualTo response
+    }
+
+    @Test
+    fun `given stable endpoint fails, when execute, then fallback to unstable endpoint`() = runTest {
+        spaceApi.givenStableEndpointThrows(httpException)
+        spaceApi.givenUnstableEndpointReturns(response)
+
+        val result = resolveSpaceInfoTask.execute(spaceApi.params)
+
+        result shouldBeEqualTo response
+    }
+
+    companion object {
+        private val response = aSpacesResponse()
+        private val httpException = HttpException(Response.error<SpacesResponse>(500, "".toResponseBody()))
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt
index 0abca8bee3dc47d7f4b29f9830699b16a8846738..149b964fd25a09f832732887a6f0334ccc2593fa 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt
@@ -21,7 +21,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.joinAll
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertEquals
 import org.junit.Test
 import org.matrix.android.sdk.MatrixTest
@@ -51,7 +51,7 @@ class CoroutineSequencersTest : MatrixTest {
                             .also { results.add(it) }
                 }
         )
-        runBlocking {
+        runTest {
             jobs.joinAll()
         }
         assertEquals(3, results.size)
@@ -81,7 +81,7 @@ class CoroutineSequencersTest : MatrixTest {
                             .also { results.add(it) }
                 }
         )
-        runBlocking {
+        runTest {
             jobs.joinAll()
         }
         assertEquals(3, results.size)
@@ -109,7 +109,7 @@ class CoroutineSequencersTest : MatrixTest {
         )
         // We are canceling the second job
         jobs[1].cancel()
-        runBlocking {
+        runTest {
             jobs.joinAll()
         }
         assertEquals(2, results.size)
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d4fc9867913965da8cc6ec101f71d5df7dcd9491
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.space.SpaceApi
+import org.matrix.android.sdk.internal.session.space.SpacesResponse
+import org.matrix.android.sdk.test.fixtures.ResolveSpaceInfoTaskParamsFixture
+
+internal class FakeSpaceApi {
+
+    val instance: SpaceApi = mockk()
+    val params = ResolveSpaceInfoTaskParamsFixture.aResolveSpaceInfoTaskParams()
+
+    fun givenStableEndpointReturns(response: SpacesResponse) {
+        coEvery { instance.getSpaceHierarchy(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } returns response
+    }
+
+    fun givenStableEndpointThrows(throwable: Throwable) {
+        coEvery { instance.getSpaceHierarchy(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } throws throwable
+    }
+
+    fun givenUnstableEndpointReturns(response: SpacesResponse) {
+        coEvery { instance.getSpaceHierarchyUnstable(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } returns response
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt
new file mode 100644
index 0000000000000000000000000000000000000000..28f8c3637dc8157cae5b31aaf13947f3106062c2
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.test.fixtures
+
+import org.matrix.android.sdk.internal.session.space.ResolveSpaceInfoTask
+
+internal object ResolveSpaceInfoTaskParamsFixture {
+    fun aResolveSpaceInfoTaskParams(
+            spaceId: String = "",
+            limit: Int? = null,
+            maxDepth: Int? = null,
+            from: String? = null,
+            suggestedOnly: Boolean? = null,
+    ) = ResolveSpaceInfoTask.Params(
+            spaceId,
+            limit,
+            maxDepth,
+            from,
+            suggestedOnly,
+    )
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0a08331114455e454e461a0d337fe9bdb33bba0b
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.test.fixtures
+
+import org.matrix.android.sdk.internal.session.space.SpaceChildSummaryResponse
+import org.matrix.android.sdk.internal.session.space.SpacesResponse
+
+internal object SpacesResponseFixture {
+    fun aSpacesResponse(
+            nextBatch: String? = null,
+            rooms: List<SpaceChildSummaryResponse>? = null,
+    ) = SpacesResponse(
+            nextBatch,
+            rooms,
+    )
+}