diff --git a/dependencies.gradle b/dependencies.gradle
index 4a076a23bd59c449d4bea2829a977a81bdf4f4a8..6cb5fac64c284f53b129fe7f499d354975ca0076 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -95,6 +95,8 @@ ext.libs = [
         ],
         markwon     : [
                 'core'                   : "io.noties.markwon:core:$markwon",
+                'extLatex'               : "io.noties.markwon:ext-latex:$markwon",
+                'inlineParser'           : "io.noties.markwon:inline-parser:$markwon",
                 'html'                   : "io.noties.markwon:html:$markwon"
         ],
         airbnb      : [
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index 25a78bc0c393d0765e98ed7680165a7c1e1e4fec..3853919bcb14b06683f990ff2801f3f88c1846bb 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -14,13 +14,6 @@ ext.groups = [
                         'com.github.Zhuinden',
                 ]
         ],
-        olm         : [
-                regex: [
-                ],
-                group: [
-                        'org.matrix.android',
-                ]
-        ],
         jitsi       : [
                 regex: [
                 ],
@@ -166,6 +159,7 @@ ext.groups = [
                         'org.junit.jupiter',
                         'org.junit.platform',
                         'org.jvnet.staxex',
+                        'org.matrix.android',
                         'org.mockito',
                         'org.mongodb',
                         'org.objenesis',
@@ -179,6 +173,7 @@ ext.groups = [
                         'org.sonatype.oss',
                         'org.testng',
                         'org.threeten',
+                        'ru.noties',
                         'xerces',
                         'xml-apis',
                 ]
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index b572b17623ff29948e601e8a73352a6e7377700e..7f6d3a91b1b1fc33924f892b7dbde34d717b0fa5 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -143,8 +143,8 @@ dependencies {
     implementation libs.arrow.core
     implementation libs.arrow.instances
 
-    // olm lib is now hosted by maven at https://gitlab.matrix.org/api/v4/projects/27/packages/maven
-    implementation 'org.matrix.android:olm:3.2.7'
+    // olm lib is now hosted in MavenCentral
+    implementation 'org.matrix.android:olm-sdk:3.2.10'
 
     // DI
     implementation libs.dagger.dagger
@@ -161,7 +161,7 @@ dependencies {
     implementation libs.apache.commonsImaging
 
     // Phone number https://github.com/google/libphonenumber
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.39'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.40'
 
     testImplementation libs.tests.junit
     testImplementation 'org.robolectric:robolectric:4.7.3'
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java
index 26920fbb35d24ad5325e1e7ebbc6c8258b89f3dd..18de66e69e2483cc4694fb00a8671b418b869ab2 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/LiveDataTestObserver.java
@@ -208,4 +208,4 @@ public final class LiveDataTestObserver<T> implements Observer<T> {
     liveData.observeForever(observer);
     return observer;
   }
-} 
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index 8e21828562fd45fa90040c3cf9d5e836a039bcc2..3cb699378fa2599cc7969e0e39f3043963a92102 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -145,36 +145,9 @@ class CommonTestHelper(context: Context) {
      * @param nbOfMessages the number of time the message will be sent
      */
     fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
-        val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
         val timeline = room.createTimeline(null, TimelineSettings(10))
         timeline.start()
-        waitWithLatch(timeout + 1_000L * nbOfMessages) { latch ->
-            val timelineListener = object : Timeline.Listener {
-                override fun onTimelineFailure(throwable: Throwable) {
-                }
-
-                override fun onNewTimelineEvents(eventIds: List<String>) {
-                    // noop
-                }
-
-                override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
-                    val newMessages = snapshot
-                            .filter { it.root.sendState == SendState.SYNCED }
-                            .filter { it.root.getClearType() == EventType.MESSAGE }
-                            .filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
-
-                    Timber.v("New synced message size: ${newMessages.size}")
-                    if (newMessages.size == nbOfMessages) {
-                        sentEvents.addAll(newMessages)
-                        // Remove listener now, if not at the next update sendEvents could change
-                        timeline.removeListener(this)
-                        latch.countDown()
-                    }
-                }
-            }
-            timeline.addListener(timelineListener)
-            sendTextMessagesBatched(room, message, nbOfMessages)
-        }
+        val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout)
         timeline.dispose()
         // Check that all events has been created
         assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong())
@@ -182,9 +155,10 @@ class CommonTestHelper(context: Context) {
     }
 
     /**
-     * Will send nb of messages provided by count parameter but waits a bit every 10 messages to avoid gap in sync
+     * Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync
      */
-    private fun sendTextMessagesBatched(room: Room, message: String, count: Int) {
+    private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long): List<TimelineEvent> {
+        val sentEvents = ArrayList<TimelineEvent>(count)
         (1 until count + 1)
                 .map { "$message #$it" }
                 .chunked(10)
@@ -192,8 +166,34 @@ class CommonTestHelper(context: Context) {
                     batchedMessages.forEach { formattedMessage ->
                         room.sendTextMessage(formattedMessage)
                     }
-                    Thread.sleep(1_000L)
+                    waitWithLatch(timeout) { latch ->
+                        val timelineListener = object : Timeline.Listener {
+
+                            override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
+                                val allSentMessages = snapshot
+                                        .filter { it.root.sendState == SendState.SYNCED }
+                                        .filter { it.root.getClearType() == EventType.MESSAGE }
+                                        .filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
+
+                                val hasSyncedAllBatchedMessages = allSentMessages
+                                        .map {
+                                            it.root.getClearContent().toModel<MessageContent>()?.body
+                                        }
+                                        .containsAll(batchedMessages)
+
+                                if (allSentMessages.size == count) {
+                                    sentEvents.addAll(allSentMessages)
+                                }
+                                if (hasSyncedAllBatchedMessages) {
+                                    timeline.removeListener(this)
+                                    latch.countDown()
+                                }
+                            }
+                        }
+                        timeline.addListener(timelineListener)
+                    }
                 }
+        return sentEvents
     }
 
     // PRIVATE METHODS *****************************************************************************
@@ -332,13 +332,6 @@ class CommonTestHelper(context: Context) {
 
     fun createEventListener(latch: CountDownLatch, predicate: (List<TimelineEvent>) -> Boolean): Timeline.Listener {
         return object : Timeline.Listener {
-            override fun onTimelineFailure(throwable: Throwable) {
-                // noop
-            }
-
-            override fun onNewTimelineEvents(eventIds: List<String>) {
-                // noop
-            }
 
             override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
                 if (predicate(snapshot)) {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
index ccea6f53b9a7b06cc3de5d0b5dc4bf4d58dbcb81..71796192a8d347b16d4985ae18a38c8de249ce8b 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
@@ -246,8 +246,7 @@ class CryptoTestHelper(private val testHelper: CommonTestHelper) {
             val bobRoomSummariesLive = bob.getRoomSummariesLive(roomSummaryQueryParams { })
             val newRoomObserver = object : Observer<List<RoomSummary>> {
                 override fun onChanged(t: List<RoomSummary>?) {
-                    val indexOfFirst = t?.indexOfFirst { it.roomId == roomId } ?: -1
-                    if (indexOfFirst != -1) {
+                    if (t?.any { it.roomId == roomId }.orFalse()) {
                         bobRoomSummariesLive.removeObserver(this)
                         latch.countDown()
                     }
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 1ed2f899773c9e70efddcfc20d766f748c61d038..8625e97902ce7ac734f5f928ce35d1dd9d3ccc47 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
@@ -49,6 +49,7 @@ class MarkdownParserTest : InstrumentedTest {
      * Create the same parser than in the RoomModule
      */
     private val markdownParser = MarkdownParser(
+            Parser.builder().build(),
             Parser.builder().build(),
             HtmlRenderer.builder().softbreak("<br />").build(),
             TextPillsUtils(
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt
deleted file mode 100644
index 7628f287c97d8c1c71d3d9fa5a939f12b14a86f0..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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.shouldBeFalse
-import org.amshove.kluent.shouldBeTrue
-import org.junit.Assert.assertTrue
-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.extensions.orFalse
-import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.events.model.toModel
-import org.matrix.android.sdk.api.session.room.model.message.MessageContent
-import org.matrix.android.sdk.api.session.room.timeline.Timeline
-import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
-import org.matrix.android.sdk.common.CommonTestHelper
-import org.matrix.android.sdk.common.CryptoTestHelper
-import org.matrix.android.sdk.common.checkSendOrder
-import timber.log.Timber
-import java.util.concurrent.CountDownLatch
-
-@RunWith(JUnit4::class)
-@FixMethodOrder(MethodSorters.JVM)
-class TimelineBackToPreviousLastForwardTest : InstrumentedTest {
-
-    private val commonTestHelper = CommonTestHelper(context())
-    private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
-
-    /**
-     * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an
-     * even contained in a previous lastForward chunk, we will be able to go back to the live
-     */
-    @Test
-    fun backToPreviousLastForwardTest() {
-        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
-
-        val aliceSession = cryptoTestData.firstSession
-        val bobSession = cryptoTestData.secondSession!!
-        val aliceRoomId = cryptoTestData.roomId
-
-        aliceSession.cryptoService().setWarnOnUnknownDevices(false)
-        bobSession.cryptoService().setWarnOnUnknownDevices(false)
-
-        val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
-        val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
-
-        val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
-        bobTimeline.start()
-
-        var roomCreationEventId: String? = null
-
-        run {
-            val lock = CountDownLatch(1)
-            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
-                Timber.e("Bob timeline updated: with ${snapshot.size} events:")
-                snapshot.forEach {
-                    Timber.w(" event ${it.root}")
-                }
-
-                roomCreationEventId = snapshot.lastOrNull()?.root?.eventId
-                // Ok, we have the 8 first messages of the initial sync (room creation and bob join event)
-                snapshot.size == 8
-            }
-
-            bobTimeline.addListener(eventsListener)
-            commonTestHelper.await(lock)
-            bobTimeline.removeAllListeners()
-
-            bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
-            bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
-        }
-
-        // Bob stop to sync
-        bobSession.stopSync()
-
-        val messageRoot = "First messages from Alice"
-
-        // Alice sends 30 messages
-        commonTestHelper.sendTextMessage(
-                roomFromAlicePOV,
-                messageRoot,
-                30)
-
-        // Bob start to sync
-        bobSession.startSync(true)
-
-        run {
-            val lock = CountDownLatch(1)
-            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
-                Timber.e("Bob timeline updated: with ${snapshot.size} events:")
-                snapshot.forEach {
-                    Timber.w(" event ${it.root}")
-                }
-
-                // Ok, we have the 10 last messages from Alice.
-                snapshot.size == 10 &&
-                        snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(messageRoot).orFalse() }
-            }
-
-            bobTimeline.addListener(eventsListener)
-            commonTestHelper.await(lock)
-            bobTimeline.removeAllListeners()
-
-            bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
-            bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
-        }
-
-        // Bob navigate to the first event (room creation event), so inside the previous last forward chunk
-        run {
-            val lock = CountDownLatch(1)
-            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
-                Timber.e("Bob timeline updated: with ${snapshot.size} events:")
-                snapshot.forEach {
-                    Timber.w(" event ${it.root}")
-                }
-
-                // The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?)
-                snapshot.size == 4
-            }
-
-            bobTimeline.addListener(eventsListener)
-
-            // Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically
-            assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null)
-
-            bobTimeline.restartWithEventId(roomCreationEventId)
-
-            commonTestHelper.await(lock)
-            bobTimeline.removeAllListeners()
-
-            bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
-            bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
-        }
-
-        // Bob scroll to the future
-        run {
-            val lock = CountDownLatch(1)
-            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
-                Timber.e("Bob timeline updated: with ${snapshot.size} events:")
-                snapshot.forEach {
-                    Timber.w(" event ${it.root}")
-                }
-
-                // Bob can see the first event of the room (so Back pagination has worked)
-                snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
-                        // 8 for room creation item, and 30 for the forward pagination
-                        snapshot.size == 38 &&
-                        snapshot.checkSendOrder(messageRoot, 30, 0)
-            }
-
-            bobTimeline.addListener(eventsListener)
-
-            bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
-
-            commonTestHelper.await(lock)
-            bobTimeline.removeAllListeners()
-
-            bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
-            bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
-        }
-        bobTimeline.dispose()
-
-        cryptoTestData.cleanUp(commonTestHelper)
-    }
-}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
index bc9722c922b89d1c78a1095faa64297d728991bd..05a43de0ac6ac9dcb0a504d14cbd030e152d172c 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
@@ -16,6 +16,8 @@
 
 package org.matrix.android.sdk.session.room.timeline
 
+import kotlinx.coroutines.runBlocking
+import org.amshove.kluent.internal.assertEquals
 import org.amshove.kluent.shouldBeFalse
 import org.amshove.kluent.shouldBeTrue
 import org.junit.FixMethodOrder
@@ -123,54 +125,29 @@ class TimelineForwardPaginationTest : InstrumentedTest {
         // Alice paginates BACKWARD and FORWARD of 50 events each
         // Then she can only navigate FORWARD
         run {
-            val lock = CountDownLatch(1)
-            val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
-                Timber.e("Alice timeline updated: with ${snapshot.size} events:")
-                snapshot.forEach {
-                    Timber.w(" event ${it.root.content}")
-                }
-
-                // Alice can see the first event of the room (so Back pagination has worked)
-                snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
-                        // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
-                        snapshot.size == 57 // 6 + 1 + 50
+            val snapshot = runBlocking {
+                aliceTimeline.awaitPaginate(Timeline.Direction.BACKWARDS, 50)
+                aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
             }
-
-            aliceTimeline.addListener(aliceEventsListener)
-
-            // Restart the timeline to the first sent event
-            // We ask to load event backward and forward
-            aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
-            aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
-
-            commonTestHelper.await(lock)
-            aliceTimeline.removeAllListeners()
-
             aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
             aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
+
+            assertEquals(EventType.STATE_ROOM_CREATE, snapshot.lastOrNull()?.root?.getClearType())
+            // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
+            // 6 + 1 + 50
+            assertEquals(57, snapshot.size)
         }
 
         // Alice paginates once again FORWARD for 50 events
         // All the timeline is retrieved, she cannot paginate anymore in both direction
         run {
-            val lock = CountDownLatch(1)
-            val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
-                Timber.e("Alice timeline updated: with ${snapshot.size} events:")
-                snapshot.forEach {
-                    Timber.w(" event ${it.root.content}")
-                }
-                // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
-                snapshot.size == 6 + numberOfMessagesToSend &&
-                        snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
-            }
-
-            aliceTimeline.addListener(aliceEventsListener)
-
             // Ask for a forward pagination
-            aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
-
-            commonTestHelper.await(lock)
-            aliceTimeline.removeAllListeners()
+            val snapshot = runBlocking {
+                aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50)
+            }
+            // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
+            snapshot.size == 6 + numberOfMessagesToSend &&
+                    snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
 
             // The timeline is fully loaded
             aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt
index e865fe17da844663d4ab73bfc24c89233ae37c14..c6fdec150d5ca7571836daee0e3479e350f18469 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt
@@ -168,10 +168,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest {
 
             bobTimeline.addListener(eventsListener)
 
-            // Restart the timeline to the first sent event, and paginate in both direction
+            // Restart the timeline to the first sent event
             bobTimeline.restartWithEventId(firstMessageFromAliceId)
-            bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
-            bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
 
             commonTestHelper.await(lock)
             bobTimeline.removeAllListeners()
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b75df9b5a211194ab8955c0b9e0e01acf80d49ac
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt
@@ -0,0 +1,104 @@
+/*
+ * 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 kotlinx.coroutines.runBlocking
+import org.amshove.kluent.internal.assertEquals
+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.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.isTextMessage
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
+import org.matrix.android.sdk.api.session.room.timeline.Timeline
+import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.TestConstants
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class TimelineSimpleBackPaginationTest : InstrumentedTest {
+
+    private val commonTestHelper = CommonTestHelper(context())
+    private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+
+    @Test
+    fun timeline_backPaginate_shouldReachEndOfTimeline() {
+        val numberOfMessagesToSent = 200
+
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val bobSession = cryptoTestData.secondSession!!
+        val roomId = cryptoTestData.roomId
+
+        aliceSession.cryptoService().setWarnOnUnknownDevices(false)
+        bobSession.cryptoService().setWarnOnUnknownDevices(false)
+
+        val roomFromAlicePOV = aliceSession.getRoom(roomId)!!
+        val roomFromBobPOV = bobSession.getRoom(roomId)!!
+
+        // Alice sends X messages
+        val message = "Message from Alice"
+        commonTestHelper.sendTextMessage(
+                roomFromAlicePOV,
+                message,
+                numberOfMessagesToSent)
+
+        val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
+        bobTimeline.start()
+
+        commonTestHelper.waitWithLatch(timeout = TestConstants.timeOutMillis * 10) {
+            val listener = object : Timeline.Listener {
+
+                override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) {
+                    if (direction == Timeline.Direction.FORWARDS) {
+                        return
+                    }
+                    if (state.hasMoreToLoad && !state.loading) {
+                        bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30)
+                    } else if (!state.hasMoreToLoad) {
+                        bobTimeline.removeListener(this)
+                        it.countDown()
+                    }
+                }
+            }
+            bobTimeline.addListener(listener)
+            bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30)
+        }
+        assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS))
+        assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS))
+
+        val onlySentEvents = runBlocking {
+            bobTimeline.getSnapshot()
+        }
+                .filter {
+                    it.root.isTextMessage()
+                }.filter {
+                    (it.root.content.toModel<MessageTextContent>())?.body?.startsWith(message).orFalse()
+                }
+        assertEquals(numberOfMessagesToSent, onlySentEvents.size)
+
+        bobTimeline.dispose()
+        cryptoTestData.cleanUp(commonTestHelper)
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt
deleted file mode 100644
index 9be0a5d5af53fb7e714cbbdfe55f4d9caf7ace77..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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 com.zhuinden.monarchy.Monarchy
-import org.matrix.android.sdk.InstrumentedTest
-
-internal class TimelineTest : InstrumentedTest {
-
-    companion object {
-        private const val ROOM_ID = "roomId"
-    }
-
-    private lateinit var monarchy: Monarchy
-
-//    @Before
-//    fun setup() {
-//        Timber.plant(Timber.DebugTree())
-//        Realm.init(context())
-//        val testConfiguration = RealmConfiguration.Builder().name("test-realm")
-//                .modules(SessionRealmModule()).build()
-//
-//        Realm.deleteRealm(testConfiguration)
-//        monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
-//        RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID)
-//    }
-//
-//    private fun createTimeline(initialEventId: String? = null): Timeline {
-//        val taskExecutor = TaskExecutor(testCoroutineDispatchers)
-//        val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
-//        val paginationTask = FakePaginationTask @Inject constructor(tokenChunkEventPersistor)
-//        val getContextOfEventTask = FakeGetContextOfEventTask @Inject constructor(tokenChunkEventPersistor)
-//        val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)
-//        val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor())
-//        return DefaultTimeline(
-//                ROOM_ID,
-//                initialEventId,
-//                monarchy.realmConfiguration,
-//                taskExecutor,
-//                getContextOfEventTask,
-//                timelineEventFactory,
-//                paginationTask,
-//                null)
-//    }
-//
-//    @Test
-//    fun backPaginate_shouldLoadMoreEvents_whenPaginateIsCalled() {
-//        val timeline = createTimeline()
-//        timeline.start()
-//        val paginationCount = 30
-//        var initialLoad = 0
-//        val latch = CountDownLatch(2)
-//        var timelineEvents: List<TimelineEvent> = emptyList()
-//        timeline.listener = object : Timeline.Listener {
-//            override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
-//                if (snapshot.isNotEmpty()) {
-//                    if (initialLoad == 0) {
-//                        initialLoad = snapshot.size
-//                    }
-//                    timelineEvents = snapshot
-//                    latch.countDown()
-//                    timeline.paginate(Timeline.Direction.BACKWARDS, paginationCount)
-//                }
-//            }
-//        }
-//        latch.await()
-//        timelineEvents.size shouldBeEqualTo initialLoad + paginationCount
-//        timeline.dispose()
-//    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b8ee36e724005cfbf654ea44c97f233208858a5b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.commonmark.ext.maths
+
+import org.commonmark.node.CustomBlock
+
+class DisplayMaths(private val delimiter: DisplayDelimiter) : CustomBlock() {
+    enum class DisplayDelimiter {
+        DOUBLE_DOLLAR,
+        SQUARE_BRACKET_ESCAPED
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.kt
new file mode 100644
index 0000000000000000000000000000000000000000..962b1b8cbf346053c870cde649a7ea5b1d00d5cf
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.commonmark.ext.maths
+
+import org.commonmark.node.CustomNode
+import org.commonmark.node.Delimited
+
+class InlineMaths(private val delimiter: InlineDelimiter) : CustomNode(), Delimited {
+    enum class InlineDelimiter {
+        SINGLE_DOLLAR,
+        ROUND_BRACKET_ESCAPED
+    }
+
+    override fun getOpeningDelimiter(): String {
+        return when (delimiter) {
+            InlineDelimiter.SINGLE_DOLLAR         -> "$"
+            InlineDelimiter.ROUND_BRACKET_ESCAPED -> "\\("
+        }
+    }
+
+    override fun getClosingDelimiter(): String {
+        return when (delimiter) {
+            InlineDelimiter.SINGLE_DOLLAR         -> "$"
+            InlineDelimiter.ROUND_BRACKET_ESCAPED -> "\\)"
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.kt
new file mode 100644
index 0000000000000000000000000000000000000000..18c0fc4284b219fed51825caf8cc782ef3430e0e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.commonmark.ext.maths
+
+import org.commonmark.Extension
+import org.commonmark.ext.maths.internal.DollarMathsDelimiterProcessor
+import org.commonmark.ext.maths.internal.MathsHtmlNodeRenderer
+import org.commonmark.parser.Parser
+import org.commonmark.renderer.html.HtmlRenderer
+
+class MathsExtension private constructor() : Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension {
+    override fun extend(parserBuilder: Parser.Builder) {
+        parserBuilder.customDelimiterProcessor(DollarMathsDelimiterProcessor())
+    }
+
+    override fun extend(rendererBuilder: HtmlRenderer.Builder) {
+        rendererBuilder.nodeRendererFactory { context -> MathsHtmlNodeRenderer(context) }
+    }
+
+    companion object {
+        fun create(): Extension {
+            return MathsExtension()
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cfd03fa8f166af00c737470d0886ee275b00c7f7
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.commonmark.ext.maths.internal
+
+import org.commonmark.ext.maths.DisplayMaths
+import org.commonmark.ext.maths.InlineMaths
+import org.commonmark.node.Text
+import org.commonmark.parser.delimiter.DelimiterProcessor
+import org.commonmark.parser.delimiter.DelimiterRun
+
+class DollarMathsDelimiterProcessor : DelimiterProcessor {
+    override fun getOpeningCharacter() = '$'
+
+    override fun getClosingCharacter() = '$'
+
+    override fun getMinLength() = 1
+
+    override fun getDelimiterUse(opener: DelimiterRun, closer: DelimiterRun): Int {
+        return if (opener.length() == 1 && closer.length() == 1) 1 // inline
+        else if (opener.length() == 2 && closer.length() == 2) 2 // display
+        else 0
+    }
+
+    override fun process(opener: Text, closer: Text, delimiterUse: Int) {
+        val maths = if (delimiterUse == 1) {
+            InlineMaths(InlineMaths.InlineDelimiter.SINGLE_DOLLAR)
+        } else {
+            DisplayMaths(DisplayMaths.DisplayDelimiter.DOUBLE_DOLLAR)
+        }
+        var tmp = opener.next
+        while (tmp != null && tmp !== closer) {
+            val next = tmp.next
+            maths.appendChild(tmp)
+            tmp = next
+        }
+        opener.insertAfter(maths)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..94652ed7ad88187c7fb6f67d31d8ad3b6729a9f1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.commonmark.ext.maths.internal
+
+import org.commonmark.ext.maths.DisplayMaths
+import org.commonmark.node.Node
+import org.commonmark.node.Text
+import org.commonmark.renderer.html.HtmlNodeRendererContext
+import org.commonmark.renderer.html.HtmlWriter
+import java.util.Collections
+
+class MathsHtmlNodeRenderer(private val context: HtmlNodeRendererContext) : MathsNodeRenderer() {
+    private val html: HtmlWriter = context.writer
+    override fun render(node: Node) {
+        val display = node.javaClass == DisplayMaths::class.java
+        val contents = node.firstChild // should be the only child
+        val latex = (contents as Text).literal
+        val attributes = context.extendAttributes(node, if (display) "div" else "span", Collections.singletonMap("data-mx-maths",
+                latex))
+        html.tag(if (display) "div" else "span", attributes)
+        html.tag("code")
+        context.render(contents)
+        html.tag("/code")
+        html.tag(if (display) "/div" else "/span")
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..55cdc05c398cc1c06d8494814dc48a73d1433e86
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.commonmark.ext.maths.internal
+
+import org.commonmark.ext.maths.DisplayMaths
+import org.commonmark.ext.maths.InlineMaths
+import org.commonmark.node.Node
+import org.commonmark.renderer.NodeRenderer
+import java.util.HashSet
+
+abstract class MathsNodeRenderer : NodeRenderer {
+    override fun getNodeTypes(): Set<Class<out Node>> {
+        val types: MutableSet<Class<out Node>> = HashSet()
+        types.add(InlineMaths::class.java)
+        types.add(DisplayMaths::class.java)
+        return types
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt
index f381ae8455a7e742f8bc45b387c89327543ba589..8ba99ad70b41762ce6a17854c8ce68016a9141e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt
@@ -27,5 +27,8 @@ enum class RoomEncryptionTrustLevel {
     Warning,
 
     // All devices in the room are verified -> the app should display a green shield
-    Trusted
+    Trusted,
+
+    // e2e is active but with an unsupported algorithm
+    E2EWithUnsupportedAlgorithm
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/EventStreamService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/EventStreamService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a1316a5444548f76fc4302d50078555f2fb52890
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/EventStreamService.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session
+
+interface EventStreamService {
+
+    fun addEventStreamListener(streamListener: LiveEventListener)
+
+    fun removeEventStreamListener(streamListener: LiveEventListener)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6fda65953acccba97022df14c10edd229285e8ca
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.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.api.session
+
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.util.JsonDict
+
+interface LiveEventListener {
+
+    fun onLiveEvent(roomId: String, event: Event)
+
+    fun onPaginatedEvent(roomId: String, event: Event)
+
+    fun onEventDecrypted(eventId: String, roomId: String, clearEvent: JsonDict)
+
+    fun onEventDecryptionError(eventId: String, roomId: String, throwable: Throwable)
+
+    fun onLiveToDeviceEvent(event: Event)
+
+    // Maybe later add more, like onJoin, onLeave..
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
index 3f817ec4d2b2b864b979b82fe6fd0c9585736cfd..36ab00731424038291c34542e85cc98f520713d5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
@@ -84,7 +84,9 @@ interface Session :
         SyncStatusService,
         HomeServerCapabilitiesService,
         SecureStorageService,
-        AccountService {
+        AccountService,
+        ToDeviceService,
+        EventStreamService {
 
     val coroutineDispatchers: MatrixCoroutineDispatchers
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..45fd39fa954593329cbcb6b544456a3081fb6c2a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session
+
+import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
+import java.util.UUID
+
+interface ToDeviceService {
+
+    /**
+     * Send an event to a specific list of devices
+     */
+    suspend fun sendToDevice(eventType: String, contentMap: MXUsersDevicesMap<Any>, txnId: String? = UUID.randomUUID().toString())
+
+    suspend fun sendToDevice(eventType: String, userId: String, deviceId: String, content: Content, txnId: String? = UUID.randomUUID().toString()) {
+        sendToDevice(eventType, mapOf(userId to listOf(deviceId)), content, txnId)
+    }
+
+    suspend fun sendToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String? = UUID.randomUUID().toString())
+
+    suspend fun sendEncryptedToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String? = UUID.randomUUID().toString())
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt
index 69b15ff7d4cb7e83bfd2c4d185af0c9b178b8cea..91167d896f0251e3d6bbafb6181dcd7a45a11fa6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt
@@ -27,4 +27,5 @@ object UserAccountDataTypes {
     const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets"
     const val TYPE_IDENTITY_SERVER = "m.identity_server"
     const val TYPE_ACCEPTED_TERMS = "m.accepted_terms"
+    const val TYPE_OVERRIDE_COLORS = "im.vector.setting.override_colors"
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/accountdata/RoomAccountDataTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/accountdata/RoomAccountDataTypes.kt
index 96eb86c0d65c7946a9f4a35efc57a40751bc4815..312fb7e1645750aa879cccc418542191b6395eed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/accountdata/RoomAccountDataTypes.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/accountdata/RoomAccountDataTypes.kt
@@ -21,4 +21,5 @@ object RoomAccountDataTypes {
     const val EVENT_TYPE_TAG = "m.tag"
     const val EVENT_TYPE_FULLY_READ = "m.fully_read"
     const val EVENT_TYPE_SPACE_ORDER = "org.matrix.msc3230.space_order" // m.space_order
+    const val EVENT_TYPE_TAGGED_EVENTS = "m.tagged_events"
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt
index 6581247b90a69dcedda5984209486747e589a3fc..445d16b72bcd2e6e20588da45562eb44506e7bcf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt
@@ -27,9 +27,12 @@ interface RoomCryptoService {
     fun shouldEncryptForInvitedMembers(): Boolean
 
     /**
-     * Enable encryption of the room
+     * Enable encryption of the room.
+     * @param Use force to ensure that this algorithm will be used. Otherwise this call
+     * will throw if encryption is already setup or if the algorithm is not supported. Only to
+     * be used by admins to fix misconfigured encryption.
      */
-    suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM)
+    suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, force: Boolean = false)
 
     /**
      * Ensures all members of the room are loaded and outbound session keys are shared.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f68121692944c1d018a160f5dc3cad526210b011
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room.model
+
+import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+
+sealed class RoomEncryptionAlgorithm {
+
+    abstract class SupportedAlgorithm(val alg: String) : RoomEncryptionAlgorithm()
+
+    object Megolm : SupportedAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM)
+
+    data class UnsupportedAlgorithm(val name: String?) : RoomEncryptionAlgorithm()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
index 10cad026bcbe1100dfbb4ecfe31d6b90d0cd6e60..c793a04f9de476581022c93a0fda03bd2efadc49 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
@@ -62,7 +62,8 @@ data class RoomSummary(
         val roomType: String? = null,
         val spaceParents: List<SpaceParentInfo>? = null,
         val spaceChildren: List<SpaceChildInfo>? = null,
-        val flattenParentIds: List<String> = emptyList()
+        val flattenParentIds: List<String> = emptyList(),
+        val roomEncryptionAlgorithm: RoomEncryptionAlgorithm? = null
 ) {
 
     val isVersioned: Boolean
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 5b387c3413e876c6c14e9fea5ed5c418666a6ebc..606500c4e7204e8b7c69c4c9be1f715e31c5c738 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
@@ -56,6 +56,15 @@ interface SendService {
      */
     fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable
 
+    /**
+     * Method to quote an events content.
+     * @param quotedEvent The event to which we will quote it's content.
+     * @param text the text message to send
+     * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
+     * @return a [Cancelable]
+     */
+    fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable
+
     /**
      * Method to send a media asynchronously.
      * @param attachment the media to send
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt
index 06c88db83162afe5a50ce55fa45c721484ea7df1..241e5f3b9b8808fcece17a52a101640bd7aba661 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt
@@ -71,14 +71,10 @@ interface Timeline {
     fun paginate(direction: Direction, count: Int)
 
     /**
-     * Returns the number of sending events
+     * This is the same than the regular paginate method but waits for the results instead
+     * of relying on the timeline listener.
      */
-    fun pendingEventCount(): Int
-
-    /**
-     * Returns the number of failed sending events.
-     */
-    fun failedToDeliverEventCount(): Int
+    suspend fun awaitPaginate(direction: Direction, count: Int): List<TimelineEvent>
 
     /**
      * Returns the index of a built event or null.
@@ -86,14 +82,14 @@ interface Timeline {
     fun getIndexOfEvent(eventId: String?): Int?
 
     /**
-     * Returns the built [TimelineEvent] at index or null
+     * Returns the current pagination state for the direction.
      */
-    fun getTimelineEventAtIndex(index: Int): TimelineEvent?
+    fun getPaginationState(direction: Direction): PaginationState
 
     /**
-     * Returns the built [TimelineEvent] with eventId or null
+     * Returns a snapshot of the timeline in his current state.
      */
-    fun getTimelineEventWithId(eventId: String?): TimelineEvent?
+    fun getSnapshot(): List<TimelineEvent>
 
     interface Listener {
         /**
@@ -101,19 +97,33 @@ interface Timeline {
          * The latest event is the first in the list
          * @param snapshot the most up to date snapshot
          */
-        fun onTimelineUpdated(snapshot: List<TimelineEvent>)
+        fun onTimelineUpdated(snapshot: List<TimelineEvent>) = Unit
 
         /**
          * Called whenever an error we can't recover from occurred
          */
-        fun onTimelineFailure(throwable: Throwable)
+        fun onTimelineFailure(throwable: Throwable) = Unit
 
         /**
          * Called when new events come through the sync
          */
-        fun onNewTimelineEvents(eventIds: List<String>)
+        fun onNewTimelineEvents(eventIds: List<String>) = Unit
+
+        /**
+         * Called when the pagination state has changed in one direction
+         */
+        fun onStateUpdated(direction: Direction, state: PaginationState) = Unit
     }
 
+    /**
+     * Pagination state
+     */
+    data class PaginationState(
+            val hasMoreToLoad: Boolean = true,
+            val loading: Boolean = false,
+            val inError: Boolean = false
+    )
+
     /**
      * This is used to paginate in one or another direction.
      */
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 932439c81c6fa94c24874562add7da74edd62f79..45dc322420fffb5c5accb97fe6856058e0479d73 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
@@ -47,6 +47,10 @@ data class TimelineEvent(
          */
         val localId: Long,
         val eventId: String,
+        /**
+         * This display index is the position in the current chunk.
+         * It's not unique on the timeline as it's reset on each chunk.
+         */
         val displayIndex: Int,
         val senderInfo: SenderInfo,
         val annotations: EventAnnotationsSummary? = null,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
index 5338e7e92f537bca9d63113311ec1b844ef05eef..82eced43711f3cf6c52a4dc43f25d658899ec7be 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
@@ -37,7 +37,6 @@ internal class CryptoSessionInfoProvider @Inject constructor(
     fun isRoomEncrypted(roomId: String): Boolean {
         val encryptionEvent = monarchy.fetchCopied { realm ->
             EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
-                    .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
                     .isEmpty(EventEntityFields.STATE_KEY)
                     .findFirst()
         }
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 7d9c35141002290d10b12201cb3c3be83ed69727..7dd8cc73ae28bf713ca79a8f743c09cc81af58ee 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
@@ -90,6 +90,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.extensions.foldToCallback
 import org.matrix.android.sdk.internal.session.SessionScope
+import org.matrix.android.sdk.internal.session.StreamEventsManager
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.TaskThread
@@ -168,14 +169,15 @@ internal class DefaultCryptoService @Inject constructor(
         private val coroutineDispatchers: MatrixCoroutineDispatchers,
         private val taskExecutor: TaskExecutor,
         private val cryptoCoroutineScope: CoroutineScope,
-        private val eventDecryptor: EventDecryptor
+        private val eventDecryptor: EventDecryptor,
+        private val liveEventManager: Lazy<StreamEventsManager>
 ) : CryptoService {
 
     private val isStarting = AtomicBoolean(false)
     private val isStarted = AtomicBoolean(false)
 
     fun onStateEvent(roomId: String, event: Event) {
-        when (event.getClearType()) {
+        when (event.type) {
             EventType.STATE_ROOM_ENCRYPTION         -> onRoomEncryptionEvent(roomId, event)
             EventType.STATE_ROOM_MEMBER             -> onRoomMembershipEvent(roomId, event)
             EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
@@ -183,10 +185,13 @@ internal class DefaultCryptoService @Inject constructor(
     }
 
     fun onLiveEvent(roomId: String, event: Event) {
-        when (event.getClearType()) {
-            EventType.STATE_ROOM_ENCRYPTION         -> onRoomEncryptionEvent(roomId, event)
-            EventType.STATE_ROOM_MEMBER             -> onRoomMembershipEvent(roomId, event)
-            EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
+        // handle state events
+        if (event.isStateEvent()) {
+            when (event.type) {
+                EventType.STATE_ROOM_ENCRYPTION         -> onRoomEncryptionEvent(roomId, event)
+                EventType.STATE_ROOM_MEMBER             -> onRoomMembershipEvent(roomId, event)
+                EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
+            }
         }
     }
 
@@ -429,7 +434,17 @@ internal class DefaultCryptoService @Inject constructor(
                     val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
                     oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
                 }
-                if (isStarted()) {
+                // 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
+                // the old otk too early. In this case we want to wait for the pending to_device before doing anything
+                // As per spec:
+                // If there is a large queue of send-to-device messages, the server should limit the number sent in each /sync response.
+                // 100 messages is recommended as a reasonable limit.
+                // The limit is not part of the spec, so it's probably safer to handle that when there are no more to_device ( so we are sure
+                // that there are no pending to_device
+                val toDevices = syncResponse.toDevice?.events.orEmpty()
+                if (isStarted() && toDevices.isEmpty()) {
                     // Make sure we process to-device messages before generating new one-time-keys #2782
                     deviceListManager.refreshOutdatedDeviceLists()
                     // The presence of device_unused_fallback_key_types indicates that the server supports fallback keys.
@@ -563,26 +578,31 @@ internal class DefaultCryptoService @Inject constructor(
         // (for now at least. Maybe we should alert the user somehow?)
         val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
 
-        if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
-            Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
+        if (existingAlgorithm == algorithm) {
+            // ignore
+            Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in  $roomId")
             return false
         }
 
         val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)
 
+        // Always store even if not supported
+        cryptoStore.storeRoomAlgorithm(roomId, algorithm)
+
         if (!encryptingClass) {
             Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm")
             return false
         }
 
-        cryptoStore.storeRoomAlgorithm(roomId, algorithm!!)
-
-        val alg: IMXEncrypting = when (algorithm) {
+        val alg: IMXEncrypting? = when (algorithm) {
             MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
-            else                      -> olmEncryptionFactory.create(roomId)
+            MXCRYPTO_ALGORITHM_OLM    -> olmEncryptionFactory.create(roomId)
+            else                      -> null
         }
 
-        roomEncryptorsStore.put(roomId, alg)
+        if (alg != null) {
+            roomEncryptorsStore.put(roomId, alg)
+        }
 
         // if encryption was not previously enabled in this room, we will have been
         // ignoring new device events for these users so far. We may well have
@@ -772,6 +792,7 @@ internal class DefaultCryptoService @Inject constructor(
                 }
             }
         }
+        liveEventManager.get().dispatchOnLiveToDevice(event)
     }
 
     /**
@@ -914,6 +935,7 @@ internal class DefaultCryptoService @Inject constructor(
     }
 
     private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
+        if (!event.isStateEvent()) return
         val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
         eventContent?.historyVisibility?.let {
             cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED)
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 8bbc71543cf25d1f0a5df94954b7bb700bd6cf10..2ee24dfbb064d29cfa93a99778fd6e88801e0ee6 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
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
+import dagger.Lazy
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
@@ -43,6 +44,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
 import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
+import org.matrix.android.sdk.internal.session.StreamEventsManager
 import timber.log.Timber
 
 private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO)
@@ -56,7 +58,8 @@ internal class MXMegolmDecryption(private val userId: String,
                                   private val cryptoStore: IMXCryptoStore,
                                   private val sendToDeviceTask: SendToDeviceTask,
                                   private val coroutineDispatchers: MatrixCoroutineDispatchers,
-                                  private val cryptoCoroutineScope: CoroutineScope
+                                  private val cryptoCoroutineScope: CoroutineScope,
+                                  private val liveEventManager: Lazy<StreamEventsManager>
 ) : IMXDecrypting, IMXWithHeldExtension {
 
     var newSessionListener: NewSessionListener? = null
@@ -108,12 +111,15 @@ internal class MXMegolmDecryption(private val userId: String,
                                         claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
                                         forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
                                                 .orEmpty()
-                                )
+                                ).also {
+                                    liveEventManager.get().dispatchLiveEventDecrypted(event, it)
+                                }
                             } else {
                                 throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
                             }
                         },
                         { throwable ->
+                            liveEventManager.get().dispatchLiveEventDecryptionFailed(event, throwable)
                             if (throwable is MXCryptoError.OlmError) {
                                 // TODO Check the value of .message
                                 if (throwable.olmException.message == "UNKNOWN_MESSAGE_INDEX") {
@@ -133,6 +139,11 @@ internal class MXMegolmDecryption(private val userId: String,
                                     if (requestKeysOnFail) {
                                         requestKeysForEvent(event, false)
                                     }
+
+                                    throw MXCryptoError.Base(
+                                            MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX,
+                                            "UNKNOWN_MESSAGE_INDEX",
+                                            null)
                                 }
 
                                 val reason = String.format(MXCryptoError.OLM_REASON, throwable.olmException.message)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
index 29f9d193f84700a34983c208a6a28f3bd7859bb9..3eba04b9f1840e55bc743d1de7fd7e146037c00e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
+import dagger.Lazy
 import kotlinx.coroutines.CoroutineScope
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
@@ -26,6 +27,7 @@ import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.StreamEventsManager
 import javax.inject.Inject
 
 internal class MXMegolmDecryptionFactory @Inject constructor(
@@ -38,7 +40,8 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
         private val cryptoStore: IMXCryptoStore,
         private val sendToDeviceTask: SendToDeviceTask,
         private val coroutineDispatchers: MatrixCoroutineDispatchers,
-        private val cryptoCoroutineScope: CoroutineScope
+        private val cryptoCoroutineScope: CoroutineScope,
+        private val eventsManager: Lazy<StreamEventsManager>
 ) {
 
     fun create(): MXMegolmDecryption {
@@ -52,6 +55,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
                 cryptoStore,
                 sendToDeviceTask,
                 coroutineDispatchers,
-                cryptoCoroutineScope)
+                cryptoCoroutineScope,
+                eventsManager)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt
index b64cd97ff601d5b1909a40dd6e4e88c0e1eb789b..dd76ae1d8e11b1d97395d6673585a304fab70b6c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt
@@ -27,7 +27,7 @@ data class EncryptionEventContent(
          * Required. The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'.
          */
         @Json(name = "algorithm")
-        val algorithm: String,
+        val algorithm: String?,
 
         /**
          * How long the session should be used before changing it. 604800000 (a week) is the recommended default.
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 9b75f88f917316491252f313bf5729455a3d97d7..82fb565377764829313f6c24d6d4ee8d71d0725f 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
@@ -230,7 +230,7 @@ internal interface IMXCryptoStore {
      * @param roomId    the id of the room.
      * @param algorithm the algorithm.
      */
-    fun storeRoomAlgorithm(roomId: String, algorithm: String)
+    fun storeRoomAlgorithm(roomId: String, algorithm: String?)
 
     /**
      * Provides the algorithm used in a dedicated room.
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 40678a6ce6415782003706c9a5b9abb6a116dbe0..33578ba06afa51333205efcbdead4a2845da5c62 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
@@ -629,7 +629,7 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
-    override fun storeRoomAlgorithm(roomId: String, algorithm: String) {
+    override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
         doRealmTransaction(realmConfiguration) {
             CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt
deleted file mode 100644
index 7341d4656a4727229f0d613421d49dd6b48ff5da..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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.internal.database
-
-import io.realm.Realm
-import io.realm.RealmConfiguration
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.session.SessionLifecycleObserver
-import org.matrix.android.sdk.internal.database.helper.nextDisplayIndex
-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.EventEntity
-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.model.deleteOnCascade
-import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
-import org.matrix.android.sdk.internal.task.TaskExecutor
-import timber.log.Timber
-import javax.inject.Inject
-
-private const val MAX_NUMBER_OF_EVENTS_IN_DB = 35_000L
-private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300
-
-/**
- * This class makes sure to stay under a maximum number of events as it makes Realm to be unusable when listening to events
- * when the database is getting too big. This will try incrementally to remove the biggest chunks until we get below the threshold.
- * We make sure to still have a minimum number of events so it's not becoming unusable.
- * So this won't work for users with a big number of very active rooms.
- */
-internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration,
-                                                   private val taskExecutor: TaskExecutor) : SessionLifecycleObserver {
-
-    override fun onSessionStarted(session: Session) {
-        taskExecutor.executorScope.launch(Dispatchers.Default) {
-            awaitTransaction(realmConfiguration) { realm ->
-                val allRooms = realm.where(RoomEntity::class.java).findAll()
-                Timber.v("There are ${allRooms.size} rooms in this session")
-                cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L)
-            }
-        }
-    }
-
-    private fun cleanUp(realm: Realm, threshold: Long) {
-        val numberOfEvents = realm.where(EventEntity::class.java).findAll().size
-        val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size
-        Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents")
-        if (threshold <= MIN_NUMBER_OF_EVENTS_BY_CHUNK || numberOfTimelineEvents < MAX_NUMBER_OF_EVENTS_IN_DB) {
-            Timber.v("Db is low enough")
-        } else {
-            val thresholdChunks = realm.where(ChunkEntity::class.java)
-                    .greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, threshold)
-                    .findAll()
-
-            Timber.v("There are ${thresholdChunks.size} chunks to clean with more than $threshold events")
-            for (chunk in thresholdChunks) {
-                val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS)
-                val thresholdDisplayIndex = maxDisplayIndex - threshold
-                val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll()
-                Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}")
-                chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size
-                eventsToRemove.forEach {
-                    val canDeleteRoot = it.root?.stateKey == null
-                    it.deleteOnCascade(canDeleteRoot)
-                }
-                // We reset the prevToken so we will need to fetch again.
-                chunk.prevToken = null
-            }
-            cleanUp(realm, (threshold / 1.5).toLong())
-        }
-    }
-}
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 2256d9310010c8da6ba7e82d05a3ec6fc2a826e4..1f45ac2a7530300f03809f9fbab5e5f86811cfee 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
@@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
 import org.matrix.android.sdk.api.session.room.model.VersioningState
 import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
+import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
+import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
 import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
@@ -54,7 +56,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
 ) : RealmMigration {
 
     companion object {
-        const val SESSION_STORE_SCHEMA_VERSION = 19L
+        const val SESSION_STORE_SCHEMA_VERSION = 21L
     }
 
     /**
@@ -86,6 +88,8 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion <= 16) migrateTo17(realm)
         if (oldVersion <= 17) migrateTo18(realm)
         if (oldVersion <= 18) migrateTo19(realm)
+        if (oldVersion <= 19) migrateTo20(realm)
+        if (oldVersion <= 20) migrateTo21(realm)
     }
 
     private fun migrateTo1(realm: DynamicRealm) {
@@ -390,4 +394,55 @@ internal class RealmSessionStoreMigration @Inject constructor(
                     }
                 }
     }
+
+    private fun migrateTo20(realm: DynamicRealm) {
+        Timber.d("Step 19 -> 20")
+
+        realm.schema.get("ChunkEntity")?.apply {
+            if (hasField("numberOfTimelineEvents")) {
+                removeField("numberOfTimelineEvents")
+            }
+            var cleanOldChunks = false
+            if (!hasField(ChunkEntityFields.NEXT_CHUNK.`$`)) {
+                cleanOldChunks = true
+                addRealmObjectField(ChunkEntityFields.NEXT_CHUNK.`$`, this)
+            }
+            if (!hasField(ChunkEntityFields.PREV_CHUNK.`$`)) {
+                cleanOldChunks = true
+                addRealmObjectField(ChunkEntityFields.PREV_CHUNK.`$`, this)
+            }
+            if (cleanOldChunks) {
+                val chunkEntities = realm.where("ChunkEntity").equalTo(ChunkEntityFields.IS_LAST_FORWARD, false).findAll()
+                chunkEntities.deleteAllFromRealm()
+            }
+        }
+    }
+
+    private fun migrateTo21(realm: DynamicRealm) {
+        Timber.d("Step 20 -> 21")
+
+        realm.schema.get("RoomSummaryEntity")
+                ?.addField(RoomSummaryEntityFields.E2E_ALGORITHM, String::class.java)
+                ?.transform { obj ->
+
+                    val encryptionContentAdapter = MoshiProvider.providesMoshi().adapter(EncryptionEventContent::class.java)
+
+                    val encryptionEvent = realm.where("CurrentStateEventEntity")
+                            .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
+                            .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION)
+                            .findFirst()
+
+                    val encryptionEventRoot = encryptionEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
+                    val algorithm = encryptionEventRoot
+                            ?.getString(EventEntityFields.CONTENT)?.let {
+                                encryptionContentAdapter.fromJson(it)?.algorithm
+                            }
+
+                    obj.setString(RoomSummaryEntityFields.E2E_ALGORITHM, algorithm)
+                    obj.setBoolean(RoomSummaryEntityFields.IS_ENCRYPTED, encryptionEvent != null)
+                    encryptionEventRoot?.getLong(EventEntityFields.ORIGIN_SERVER_TS)?.let {
+                        obj.setLong(RoomSummaryEntityFields.ENCRYPTION_EVENT_TS, it)
+                    }
+                }
+    }
 }
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 f74e4b0f4c6dc712b382ccb438b9003c53adc4d0..c21bf74d93440a2a96350676d467cc7f86ddfc03 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
@@ -110,7 +110,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
             true
         }
     }
-    numberOfTimelineEvents++
+    // numberOfTimelineEvents++
     timelineEvents.add(timelineEventEntity)
 }
 
@@ -191,3 +191,29 @@ internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int {
         }
     }
 }
+
+internal fun ChunkEntity.doesNextChunksVerifyCondition(linkCondition: (ChunkEntity) -> Boolean): Boolean {
+    var nextChunkToCheck = this.nextChunk
+    while (nextChunkToCheck != null) {
+        if (linkCondition(nextChunkToCheck)) {
+            return true
+        }
+        nextChunkToCheck = nextChunkToCheck.nextChunk
+    }
+    return false
+}
+
+internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity): Boolean {
+    if (this.isLastForward) return true
+    if (chunkToCheck.isLastForward) return false
+    // Check if the chunk to check is linked to this one
+    if (chunkToCheck.doesNextChunksVerifyCondition { it == this }) {
+        return true
+    }
+    // Otherwise check if this chunk is linked to last forward
+    if (this.doesNextChunksVerifyCondition { it.isLastForward }) {
+        return true
+    }
+    // We don't know, so we assume it's false
+    return false
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt
index 3993e8e7991b6cc643ab2a4de242138dc5339a74..1d2cbcad51d08f655fa841bd902ceec08a9c59c1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt
@@ -28,3 +28,13 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
         currentIdNum.toLong() + 1
     }
 }
+
+internal fun TimelineEventEntity.isMoreRecentThan(eventToCheck: TimelineEventEntity): Boolean {
+    val currentChunk = this.chunk?.first() ?: return false
+    val chunkToCheck = eventToCheck.chunk?.firstOrNull() ?: return false
+    return if (currentChunk == chunkToCheck) {
+        this.displayIndex >= eventToCheck.displayIndex
+    } else {
+        currentChunk.isMoreRecentThan(chunkToCheck)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
index 3a15e0acf01e6813394348f02faa69ad6704c845..63b326096a8c0deb47b3b00c54717829b1a4ace9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
@@ -16,12 +16,15 @@
 
 package org.matrix.android.sdk.internal.database.mapper
 
+import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
+import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm
 import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
 import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
 import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
+import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.presence.toUserPresence
 import javax.inject.Inject
@@ -68,7 +71,9 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
                 isEncrypted = roomSummaryEntity.isEncrypted,
                 encryptionEventTs = roomSummaryEntity.encryptionEventTs,
                 breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
-                roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
+                roomEncryptionTrustLevel = if (roomSummaryEntity.isEncrypted && roomSummaryEntity.e2eAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) {
+                    RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm
+                } else roomSummaryEntity.roomEncryptionTrustLevel,
                 inviterId = roomSummaryEntity.inviterId,
                 hasFailedSending = roomSummaryEntity.hasFailedSending,
                 roomType = roomSummaryEntity.roomType,
@@ -99,7 +104,13 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
                             worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC
                     )
                 },
-                flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList()
+                flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList(),
+                roomEncryptionAlgorithm = when (val alg = roomSummaryEntity.e2eAlgorithm) {
+                    // I should probably use #hasEncryptorClassForAlgorithm but it says it supports
+                    // OLM which is some legacy? Now only megolm allowed in rooms
+                    MXCRYPTO_ALGORITHM_MEGOLM -> RoomEncryptionAlgorithm.Megolm
+                    else                      -> RoomEncryptionAlgorithm.UnsupportedAlgorithm(alg)
+                }
         )
     }
 }
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 68533a3c1995f6da4f0da25f620b76297bcd4954..ecb602019aba1e9956d989ed57a9da9f7aecbe4a 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
@@ -27,9 +27,10 @@ import org.matrix.android.sdk.internal.extensions.clearWith
 internal open class ChunkEntity(@Index var prevToken: String? = null,
         // Because of gaps we can have several chunks with nextToken == null
                                 @Index var nextToken: String? = null,
+                                var prevChunk: ChunkEntity? = null,
+                                var nextChunk: ChunkEntity? = null,
                                 var stateEvents: RealmList<EventEntity> = RealmList(),
                                 var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
-                                var numberOfTimelineEvents: Long = 0,
         // Only one chunk will have isLastForward == true
                                 @Index var isLastForward: Boolean = false,
                                 @Index var isLastBackward: Boolean = false
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 836fc4efaf9947569e20aee8dfb5a3eef1b51b18..ce2d1efc1d21d17a5c8f213d5640171a10e9d630 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
@@ -40,8 +40,6 @@ internal open class EventEntity(@Index var eventId: String = "",
                                 var unsignedData: String? = null,
                                 var redacts: String? = null,
                                 var decryptionResultJson: String? = null,
-                                var decryptionErrorCode: String? = null,
-                                var decryptionErrorReason: String? = null,
                                 var ageLocalTs: Long? = null
 ) : RealmObject() {
 
@@ -55,6 +53,16 @@ internal open class EventEntity(@Index var eventId: String = "",
             sendStateStr = value.name
         }
 
+    var decryptionErrorCode: String? = null
+        set(value) {
+            if (value != field) field = value
+        }
+
+    var decryptionErrorReason: String? = null
+        set(value) {
+            if (value != field) field = value
+        }
+
     companion object
 
     fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
index 67672f03add0d75342ce7f78d6377c7725f795e3..febedc34563b8646967c667a19522638f69d1890 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
@@ -205,6 +205,11 @@ internal open class RoomSummaryEntity(
             if (value != field) field = value
         }
 
+    var e2eAlgorithm: String? = null
+        set(value) {
+            if (value != field) field = value
+        }
+
     var encryptionEventTs: Long? = 0
         set(value) {
             if (value != field) field = value
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 30bbde70c2e4c6a201dce560bde2b9a8e6eeec61..185f0e2dcc4bd52ff202879a0c37729c884bee8b 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
@@ -46,7 +46,5 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) {
     if (canDeleteRoot) {
         root?.deleteFromRealm()
     }
-    annotations?.deleteOnCascade()
-    readReceipts?.deleteOnCascade()
     deleteFromRealm()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
index 60096777d9ff43e715b29fa8f5b26a4fa2b60e53..c9c96b9cc10b55213771fab7bf9d14cfc7ed22d3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.query
 import io.realm.Realm
 import io.realm.RealmConfiguration
 import org.matrix.android.sdk.api.session.events.model.LocalEcho
+import org.matrix.android.sdk.internal.database.helper.isMoreRecentThan
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
@@ -33,28 +34,26 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration,
     if (LocalEcho.isLocalEchoId(eventId)) {
         return true
     }
+    // If we don't know if the event has been read, we assume it's not
     var isEventRead = false
 
     Realm.getInstance(realmConfiguration).use { realm ->
-        val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@use
-        val eventToCheck = liveChunk.timelineEvents.find(eventId)
+        val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, true)
+        // If latest event is from you we are sure the event is read
+        if (latestEvent?.root?.sender == userId) {
+            return true
+        }
+        val eventToCheck = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
         isEventRead = when {
-            eventToCheck == null                -> hasReadMissingEvent(
-                    realm = realm,
-                    latestChunkEntity = liveChunk,
-                    roomId = roomId,
-                    userId = userId,
-                    eventId = eventId
-            )
+            eventToCheck == null                -> false
             eventToCheck.root?.sender == userId -> true
             else                                -> {
                 val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@use
-                val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex ?: Int.MIN_VALUE
-                eventToCheck.displayIndex <= readReceiptIndex
+                val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() ?: return@use
+                readReceiptEvent.isMoreRecentThan(eventToCheck)
             }
         }
     }
-
     return isEventRead
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultEventStreamService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultEventStreamService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ed21e9f1c6271b561421a078212dfdf2304c26b5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultEventStreamService.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session
+
+import org.matrix.android.sdk.api.session.EventStreamService
+import org.matrix.android.sdk.api.session.LiveEventListener
+import javax.inject.Inject
+
+internal class DefaultEventStreamService @Inject constructor(
+        private val streamEventsManager: StreamEventsManager
+) : EventStreamService {
+
+    override fun addEventStreamListener(streamListener: LiveEventListener) {
+        streamEventsManager.addLiveEventListener(streamListener)
+    }
+
+    override fun removeEventStreamListener(streamListener: LiveEventListener) {
+        streamEventsManager.removeLiveEventListener(streamListener)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
index c07ff48cf48569dd3703bb69cb36c5d61c5ba16a..1e533158a76d2454996e0d4574265ffc2710a4ba 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
@@ -27,8 +27,10 @@ import org.matrix.android.sdk.api.auth.data.SessionParams
 import org.matrix.android.sdk.api.failure.GlobalError
 import org.matrix.android.sdk.api.federation.FederationService
 import org.matrix.android.sdk.api.pushrules.PushRuleService
+import org.matrix.android.sdk.api.session.EventStreamService
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.SessionLifecycleObserver
+import org.matrix.android.sdk.api.session.ToDeviceService
 import org.matrix.android.sdk.api.session.account.AccountService
 import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
 import org.matrix.android.sdk.api.session.cache.CacheService
@@ -133,6 +135,8 @@ internal class DefaultSession @Inject constructor(
         private val spaceService: Lazy<SpaceService>,
         private val openIdService: Lazy<OpenIdService>,
         private val presenceService: Lazy<PresenceService>,
+        private val toDeviceService: Lazy<ToDeviceService>,
+        private val eventStreamService: Lazy<EventStreamService>,
         @UnauthenticatedWithCertificate
         private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
 ) : Session,
@@ -152,7 +156,9 @@ internal class DefaultSession @Inject constructor(
         HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
         ProfileService by profileService.get(),
         PresenceService by presenceService.get(),
-        AccountService by accountService.get() {
+        AccountService by accountService.get(),
+        ToDeviceService by toDeviceService.get(),
+        EventStreamService by eventStreamService.get() {
 
     override val sharedSecretStorageService: SharedSecretStorageService
         get() = _sharedSecretStorageService.get()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1615b8eef9aeeb8ca6a2dc0da313c915eeab1cb3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt
@@ -0,0 +1,73 @@
+/*
+ * 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
+
+import org.matrix.android.sdk.api.session.ToDeviceService
+import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
+import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
+import javax.inject.Inject
+
+internal class DefaultToDeviceService @Inject constructor(
+        private val sendToDeviceTask: SendToDeviceTask,
+        private val messageEncrypter: MessageEncrypter,
+        private val cryptoStore: IMXCryptoStore
+) : ToDeviceService {
+
+    override suspend fun sendToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String?) {
+        val sendToDeviceMap = MXUsersDevicesMap<Any>()
+        targets.forEach { (userId, deviceIdList) ->
+            deviceIdList.forEach { deviceId ->
+                sendToDeviceMap.setObject(userId, deviceId, content)
+            }
+        }
+        sendToDevice(eventType, sendToDeviceMap, txnId)
+    }
+
+    override suspend fun sendToDevice(eventType: String, contentMap: MXUsersDevicesMap<Any>, txnId: String?) {
+        sendToDeviceTask.executeRetry(
+                SendToDeviceTask.Params(
+                        eventType = eventType,
+                        contentMap = contentMap,
+                        transactionId = txnId
+                ),
+                3
+        )
+    }
+
+    override suspend fun sendEncryptedToDevice(eventType: String, targets: Map<String, List<String>>, content: Content, txnId: String?) {
+        val payloadJson = mapOf(
+                "type" to eventType,
+                "content" to content
+        )
+        val sendToDeviceMap = MXUsersDevicesMap<Any>()
+
+        // Should I do an ensure olm session?
+        targets.forEach { (userId, deviceIdList) ->
+            deviceIdList.forEach { deviceId ->
+                cryptoStore.getUserDevice(userId, deviceId)?.let { deviceInfo ->
+                    sendToDeviceMap.setObject(userId, deviceId, messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)))
+                }
+            }
+        }
+
+        sendToDevice(EventType.ENCRYPTED, sendToDeviceMap, txnId)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
index ebc2176a1308744e4658b2b703ffb71331ee2442..531dea1d5a6591aa68d17d7e7a868a505639af07 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
@@ -32,8 +32,10 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.api.auth.data.SessionParams
 import org.matrix.android.sdk.api.auth.data.sessionId
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
+import org.matrix.android.sdk.api.session.EventStreamService
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.SessionLifecycleObserver
+import org.matrix.android.sdk.api.session.ToDeviceService
 import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
 import org.matrix.android.sdk.api.session.events.EventService
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
@@ -47,7 +49,6 @@ import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorage
 import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
 import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
 import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
-import org.matrix.android.sdk.internal.database.DatabaseCleaner
 import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
 import org.matrix.android.sdk.internal.database.RealmSessionProvider
 import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
@@ -339,10 +340,6 @@ internal abstract class SessionModule {
     @IntoSet
     abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver
 
-    @Binds
-    @IntoSet
-    abstract fun bindDatabaseCleaner(cleaner: DatabaseCleaner): SessionLifecycleObserver
-
     @Binds
     @IntoSet
     abstract fun bindRealmSessionProvider(provider: RealmSessionProvider): SessionLifecycleObserver
@@ -379,6 +376,12 @@ internal abstract class SessionModule {
     @Binds
     abstract fun bindOpenIdTokenService(service: DefaultOpenIdService): OpenIdService
 
+    @Binds
+    abstract fun bindToDeviceService(service: DefaultToDeviceService): ToDeviceService
+
+    @Binds
+    abstract fun bindEventStreamService(service: DefaultEventStreamService): EventStreamService
+
     @Binds
     abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bb0ca1144542090f8deb18b31f79c795a5a80e16
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
@@ -0,0 +1,101 @@
+/*
+ * 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
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.LiveEventListener
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
+import timber.log.Timber
+import javax.inject.Inject
+
+@SessionScope
+internal class StreamEventsManager @Inject constructor() {
+
+    private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+
+    private val listeners = mutableListOf<LiveEventListener>()
+
+    fun addLiveEventListener(listener: LiveEventListener) {
+        listeners.add(listener)
+    }
+
+    fun removeLiveEventListener(listener: LiveEventListener) {
+        listeners.remove(listener)
+    }
+
+    fun dispatchLiveEventReceived(event: Event, roomId: String, initialSync: Boolean) {
+        Timber.v("## dispatchLiveEventReceived ${event.eventId}")
+        coroutineScope.launch {
+            if (!initialSync) {
+                listeners.forEach {
+                    tryOrNull {
+                        it.onLiveEvent(roomId, event)
+                    }
+                }
+            }
+        }
+    }
+
+    fun dispatchPaginatedEventReceived(event: Event, roomId: String) {
+        Timber.v("## dispatchPaginatedEventReceived ${event.eventId}")
+        coroutineScope.launch {
+            listeners.forEach {
+                tryOrNull {
+                    it.onPaginatedEvent(roomId, event)
+                }
+            }
+        }
+    }
+
+    fun dispatchLiveEventDecrypted(event: Event, result: MXEventDecryptionResult) {
+        Timber.v("## dispatchLiveEventDecrypted ${event.eventId}")
+        coroutineScope.launch {
+            listeners.forEach {
+                tryOrNull {
+                    it.onEventDecrypted(event.eventId ?: "", event.roomId ?: "", result.clearEvent)
+                }
+            }
+        }
+    }
+
+    fun dispatchLiveEventDecryptionFailed(event: Event, error: Throwable) {
+        Timber.v("## dispatchLiveEventDecryptionFailed ${event.eventId}")
+        coroutineScope.launch {
+            listeners.forEach {
+                tryOrNull {
+                    it.onEventDecryptionError(event.eventId ?: "", event.roomId ?: "", error)
+                }
+            }
+        }
+    }
+
+    fun dispatchOnLiveToDevice(event: Event) {
+        Timber.v("## dispatchOnLiveToDevice ${event.eventId}")
+        coroutineScope.launch {
+            listeners.forEach {
+                tryOrNull {
+                    it.onLiveToDeviceEvent(event)
+                }
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt
index 1b0ccbb48956688f75e80f32345a85b3e1a96a5c..b988f2253cdd92e82f5fd1a87e7d99fad6a64129 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt
@@ -109,18 +109,23 @@ internal class FileUploader @Inject constructor(
                               filename: String?,
                               mimeType: String?,
                               progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
-        val inputStream = withContext(Dispatchers.IO) {
-            context.contentResolver.openInputStream(uri)
-        } ?: throw FileNotFoundException()
-        val workingFile = temporaryFileCreator.create()
-        workingFile.outputStream().use {
-            inputStream.copyTo(it)
-        }
+        val workingFile = context.copyUriToTempFile(uri)
         return uploadFile(workingFile, filename, mimeType, progressListener).also {
             tryOrNull { workingFile.delete() }
         }
     }
 
+    private suspend fun Context.copyUriToTempFile(uri: Uri): File {
+        return withContext(Dispatchers.IO) {
+            val inputStream = contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
+            val workingFile = temporaryFileCreator.create()
+            workingFile.outputStream().use {
+                inputStream.copyTo(it)
+            }
+            workingFile
+        }
+    }
+
     private suspend fun upload(uploadBody: RequestBody,
                                filename: String?,
                                progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
index a19832c5230dbdb68ce7003edcc12950978a7665..caf415865795baf9d8813a9488451f961842a987 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
@@ -68,7 +68,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
     }
 
     override suspend fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String) {
-        withContext(coroutineDispatchers.main) {
+        withContext(coroutineDispatchers.io) {
             val response = fileUploader.uploadFromUri(newAvatarUri, fileName, MimeTypes.Jpeg)
             setAvatarUrlTask.execute(SetAvatarUrlTask.Params(userId = userId, newAvatarUrl = response.contentUri))
             userStore.updateAvatar(userId, response.contentUri)
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 cb4bcdb606a24975bc8c49266fe04c06d585ea69..1fe7503141de9d612020d4cf7c34a46efb0ecd38 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
@@ -119,15 +119,15 @@ internal class DefaultRoom(override val roomId: String,
         }
     }
 
-    override suspend fun enableEncryption(algorithm: String) {
+    override suspend fun enableEncryption(algorithm: String, force: Boolean) {
         when {
-            isEncrypted()                          -> {
+            (!force && isEncrypted() && encryptionAlgorithm() == MXCRYPTO_ALGORITHM_MEGOLM) -> {
                 throw IllegalStateException("Encryption is already enabled for this room")
             }
-            algorithm != MXCRYPTO_ALGORITHM_MEGOLM -> {
+            (!force && algorithm != MXCRYPTO_ALGORITHM_MEGOLM)                              -> {
                 throw InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported")
             }
-            else                                   -> {
+            else                                                                            -> {
                 val params = SendStateTask.Params(
                         roomId = roomId,
                         stateKey = null,
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 dbd0ae6f060a1e011af0207de53bb023eb0aeb5e..64f6bc0b30db4f82c8c694fb4dfe09944d94056f 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
@@ -19,6 +19,9 @@ package org.matrix.android.sdk.internal.session.room
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
+import org.commonmark.Extension
+import org.commonmark.ext.maths.MathsExtension
+import org.commonmark.node.BlockQuote
 import org.commonmark.parser.Parser
 import org.commonmark.renderer.html.HtmlRenderer
 import org.matrix.android.sdk.api.session.file.FileService
@@ -98,12 +101,29 @@ import org.matrix.android.sdk.internal.session.room.version.DefaultRoomVersionUp
 import org.matrix.android.sdk.internal.session.room.version.RoomVersionUpgradeTask
 import org.matrix.android.sdk.internal.session.space.DefaultSpaceService
 import retrofit2.Retrofit
+import javax.inject.Qualifier
+
+/**
+ * Used to inject the simple commonmark Parser
+ */
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+internal annotation class SimpleCommonmarkParser
+
+/**
+ * Used to inject the advanced commonmark Parser
+ */
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+internal annotation class AdvancedCommonmarkParser
 
 @Module
 internal abstract class RoomModule {
 
     @Module
     companion object {
+        private val extensions: List<Extension> = listOf(MathsExtension.create())
+
         @Provides
         @JvmStatic
         @SessionScope
@@ -119,9 +139,21 @@ internal abstract class RoomModule {
         }
 
         @Provides
+        @AdvancedCommonmarkParser
         @JvmStatic
-        fun providesParser(): Parser {
-            return Parser.builder().build()
+        fun providesAdvancedParser(): Parser {
+            return Parser.builder().extensions(extensions).build()
+        }
+
+        @Provides
+        @SimpleCommonmarkParser
+        @JvmStatic
+        fun providesSimpleParser(): Parser {
+            // The simple parser disables all blocks but quotes.
+            // Inline parsing(bold, italic, etc) is also enabled and is not easy to disable in commonmark currently.
+            return Parser.builder()
+                    .enabledBlockTypes(setOf(BlockQuote::class.java))
+                    .build()
         }
 
         @Provides
@@ -129,6 +161,7 @@ internal abstract class RoomModule {
         fun providesHtmlRenderer(): HtmlRenderer {
             return HtmlRenderer
                     .builder()
+                    .extensions(extensions)
                     .softbreak("<br />")
                     .build()
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt
index 28f55a01eeb059d75a129113ef8182914eb45f0b..b30c66c82ed9c3e956b08f316c0681bac7d77d30 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt
@@ -34,12 +34,10 @@ import org.matrix.android.sdk.internal.database.query.isEventRead
 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.task.TaskExecutor
 
 internal class DefaultReadService @AssistedInject constructor(
         @Assisted private val roomId: String,
         @SessionDatabase private val monarchy: Monarchy,
-        private val taskExecutor: TaskExecutor,
         private val setReadMarkersTask: SetReadMarkersTask,
         private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
         @UserId private val userId: String
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt
index 5f6ebc68c2b7a46e24ce813fb31cd73cce6896a6..fe180536c89bb24a5d959bdd4b5237db9d72bce3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt
@@ -44,10 +44,10 @@ internal class CancelSendTracker @Inject constructor() {
     }
 
     fun isCancelRequestedFor(eventId: String?, roomId: String?): Boolean {
-        val index = synchronized(cancellingRequests) {
-            cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId }
+        val found = synchronized(cancellingRequests) {
+            cancellingRequests.any { it.localId == eventId && it.roomId == roomId }
         }
-        return index != -1
+        return found
     }
 
     fun markCancelled(eventId: String, roomId: String) {
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 d3162aef796c76e2034e174463dae9b60673f091..fb2fb3950af0b6c410cfa4f52d1a4507737d1ca3 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
@@ -97,6 +97,12 @@ internal class DefaultSendService @AssistedInject constructor(
                 .let { sendEvent(it) }
     }
 
+    override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean): Cancelable {
+        return localEchoEventFactory.createQuotedTextEvent(roomId, quotedEvent, text, autoMarkdown)
+                .also { createLocalEcho(it) }
+                .let { sendEvent(it) }
+    }
+
     override fun sendPoll(question: String, options: List<String>): Cancelable {
         return localEchoEventFactory.createPollEvent(roomId, question, options)
                 .also { createLocalEcho(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 85b22628d7813b939e3fc5663a3996751db68f59..c4caedc407769754c9e200f7c3a5d76aa9423d39 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
@@ -198,20 +198,23 @@ internal class LocalEchoEventFactory @Inject constructor(
                                  eventReplaced: TimelineEvent,
                                  originalEvent: TimelineEvent,
                                  newBodyText: String,
-                                 newBodyAutoMarkdown: Boolean,
+                                 autoMarkdown: Boolean,
                                  msgType: String,
                                  compatibilityText: String): Event {
         val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "", false)
         val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: ""
 
         val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.isReply())
-        val replyFormatted = REPLY_PATTERN.format(
+        // As we always supply formatted body for replies we should force the MarkdownParser to produce html.
+        val newBodyFormatted = markdownParser.parse(newBodyText, force = true, advanced = autoMarkdown).takeFormatted()
+        // Body of the original message may not have formatted version, so may also have to convert to html.
+        val bodyFormatted = body.formattedText ?: markdownParser.parse(body.text, force = true, advanced = autoMarkdown).takeFormatted()
+        val replyFormatted = buildFormattedReply(
                 permalink,
                 userLink,
                 originalEvent.senderInfo.disambiguatedDisplayName,
-                // Remove inner mx_reply tags if any
-                body.takeFormatted().replace(MX_REPLY_REGEX, ""),
-                createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
+                bodyFormatted,
+                newBodyFormatted
         )
         //
         // > <@alice:example.org> This is the original body
@@ -391,13 +394,17 @@ internal class LocalEchoEventFactory @Inject constructor(
         val userLink = permalinkFactory.createPermalink(userId, false) ?: return null
 
         val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply())
-        val replyFormatted = REPLY_PATTERN.format(
+
+        // As we always supply formatted body for replies we should force the MarkdownParser to produce html.
+        val replyTextFormatted = markdownParser.parse(replyText, force = true, advanced = autoMarkdown).takeFormatted()
+        // Body of the original message may not have formatted version, so may also have to convert to html.
+        val bodyFormatted = body.formattedText ?: markdownParser.parse(body.text, force = true, advanced = autoMarkdown).takeFormatted()
+        val replyFormatted = buildFormattedReply(
                 permalink,
                 userLink,
                 userId,
-                // Remove inner mx_reply tags if any
-                body.takeFormatted().replace(MX_REPLY_REGEX, ""),
-                createTextContent(replyText, autoMarkdown).takeFormatted()
+                bodyFormatted,
+                replyTextFormatted
         )
         //
         // > <@alice:example.org> This is the original body
@@ -415,6 +422,16 @@ internal class LocalEchoEventFactory @Inject constructor(
         return createMessageEvent(roomId, content)
     }
 
+    private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
+        return REPLY_PATTERN.format(
+                permalink,
+                userLink,
+                userId,
+                // Remove inner mx_reply tags if any
+                bodyFormatted.replace(MX_REPLY_REGEX, ""),
+                newBodyFormatted
+        )
+    }
     private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
         return buildString {
             append("> <")
@@ -498,6 +515,38 @@ internal class LocalEchoEventFactory @Inject constructor(
         localEchoRepository.createLocalEcho(event)
     }
 
+    fun createQuotedTextEvent(
+            roomId: String,
+            quotedEvent: TimelineEvent,
+            text: String,
+            autoMarkdown: Boolean,
+    ): Event {
+        val messageContent = quotedEvent.getLastMessageContent()
+        val textMsg = messageContent?.body
+        val quoteText = legacyRiotQuoteText(textMsg, text)
+        return createFormattedTextEvent(roomId, markdownParser.parse(quoteText, force = true, advanced = autoMarkdown), MessageType.MSGTYPE_TEXT)
+    }
+
+    private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
+        val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
+        return buildString {
+            if (messageParagraphs != null) {
+                for (i in messageParagraphs.indices) {
+                    if (messageParagraphs[i].isNotBlank()) {
+                        append("> ")
+                        append(messageParagraphs[i])
+                    }
+
+                    if (i != messageParagraphs.lastIndex) {
+                        append("\n\n")
+                    }
+                }
+            }
+            append("\n\n")
+            append(myText)
+        }
+    }
+
     companion object {
         // <mx-reply>
         //     <blockquote>
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt
index c99d482300a2253b838ea30d084b7c35df51fd7e..ef7945cf8cc5416858e81cd7a5bbc66ff5696e72 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.session.room.send
 
 import org.commonmark.parser.Parser
 import org.commonmark.renderer.html.HtmlRenderer
+import org.matrix.android.sdk.internal.session.room.AdvancedCommonmarkParser
+import org.matrix.android.sdk.internal.session.room.SimpleCommonmarkParser
 import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
 import javax.inject.Inject
 
@@ -27,22 +29,30 @@ import javax.inject.Inject
  * If any change is required, please add a test covering the problem and make sure all the tests are still passing.
  */
 internal class MarkdownParser @Inject constructor(
-        private val parser: Parser,
+        @AdvancedCommonmarkParser private val advancedParser: Parser,
+        @SimpleCommonmarkParser private val simpleParser: Parser,
         private val htmlRenderer: HtmlRenderer,
         private val textPillsUtils: TextPillsUtils
 ) {
 
-    private val mdSpecialChars = "[`_\\-*>.\\[\\]#~]".toRegex()
+    private val mdSpecialChars = "[`_\\-*>.\\[\\]#~$]".toRegex()
 
-    fun parse(text: CharSequence): TextContent {
+    /**
+     * Parses some input text and produces html.
+     * @param text An input CharSequence to be parsed.
+     * @param force Skips the check for detecting if the input contains markdown and always converts to html.
+     * @param advanced Whether to use the full markdown support or the simple version.
+     * @return TextContent containing the plain text and the formatted html if generated.
+     */
+    fun parse(text: CharSequence, force: Boolean = false, advanced: Boolean = true): TextContent {
         val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString()
 
         // If no special char are detected, just return plain text
-        if (source.contains(mdSpecialChars).not()) {
+        if (!force && source.contains(mdSpecialChars).not()) {
             return TextContent(source)
         }
 
-        val document = parser.parse(source)
+        val document = if (advanced) advancedParser.parse(source) else simpleParser.parse(source)
         val htmlText = htmlRenderer.render(document)
 
         // Cleanup extra paragraph
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 3556cabb3354eda1fdb42c1157afe179b782f9c0..a7887d77f87680af0ec0bc274eec2fd946390ea7 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 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
 import org.matrix.android.sdk.api.session.events.model.toModel
@@ -37,13 +38,11 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
 import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications
 import org.matrix.android.sdk.internal.crypto.EventDecryptor
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
+import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
 import org.matrix.android.sdk.internal.database.mapper.ContentMapper
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
-import org.matrix.android.sdk.internal.database.model.EventEntity
-import org.matrix.android.sdk.internal.database.model.EventEntityFields
 import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
@@ -56,7 +55,6 @@ 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.isEventRead
 import org.matrix.android.sdk.internal.database.query.where
-import org.matrix.android.sdk.internal.database.query.whereType
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.extensions.clearWith
 import org.matrix.android.sdk.internal.query.process
@@ -122,10 +120,8 @@ internal class RoomSummaryUpdater @Inject constructor(
         Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]")
 
         // Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
-        val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
-                .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
-                .isNotNull(EventEntityFields.STATE_KEY)
-                .findFirst()
+        val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
+        Timber.v("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
 
         val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
 
@@ -136,7 +132,7 @@ internal class RoomSummaryUpdater @Inject constructor(
 
         roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 ||
                 // avoid this call if we are sure there are unread events
-                !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId)
+                latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId) } ?: false
 
         roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId))
         roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId)
@@ -151,6 +147,11 @@ internal class RoomSummaryUpdater @Inject constructor(
                 .orEmpty()
         roomSummaryEntity.updateAliases(roomAliases)
         roomSummaryEntity.isEncrypted = encryptionEvent != null
+
+        roomSummaryEntity.e2eAlgorithm = ContentMapper.map(encryptionEvent?.content)
+                ?.toModel<EncryptionEventContent>()
+                ?.algorithm
+
         roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs
 
         if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) {
@@ -236,7 +237,7 @@ internal class RoomSummaryUpdater @Inject constructor(
                                     .findFirst()
                                     ?.let { childSum ->
                                         lookupMap.entries.firstOrNull { it.key.roomId == lookedUp.roomId }?.let { entry ->
-                                            if (entry.value.indexOfFirst { it.roomId == childSum.roomId } == -1) {
+                                            if (entry.value.none { it.roomId == childSum.roomId }) {
                                                 // add looked up as a parent
                                                 entry.value.add(childSum)
                                             }
@@ -299,7 +300,7 @@ internal class RoomSummaryUpdater @Inject constructor(
                                                 .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
                                                 .findFirst()
                                                 ?.let { parentSum ->
-                                                    if (lookupMap[parentSum]?.indexOfFirst { it.roomId == lookedUp.roomId } == -1) {
+                                                    if (lookupMap[parentSum]?.none { it.roomId == lookedUp.roomId }.orFalse()) {
                                                         // add lookedup as a parent
                                                         lookupMap[parentSum]?.add(lookedUp)
                                                     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/taggedevents/TaggedEventsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/taggedevents/TaggedEventsContent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1b19d27e1d4094ae9ae65a837d951759fdd4ed3d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/taggedevents/TaggedEventsContent.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.taggedevents
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * Keys are event IDs, values are event information.
+ */
+typealias TaggedEvent = Map<String, TaggedEventInfo>
+
+/**
+ * Keys are tagged event names (eg. m.favourite), values are the related events.
+ */
+typealias TaggedEvents = Map<String, TaggedEvent>
+
+/**
+ * Class used to parse the content of a m.tagged_events type event.
+ * This kind of event defines the tagged events in a room.
+ *
+ * The content of this event is a tags key whose value is an object mapping the name of each tag
+ * to another object. The JSON object associated with each tag is an object where the keys are the
+ * event IDs and values give information about the events.
+ *
+ * Ref: https://github.com/matrix-org/matrix-doc/pull/2437
+ */
+@JsonClass(generateAdapter = true)
+data class TaggedEventsContent(
+        @Json(name = "tags")
+        var tags: TaggedEvents = emptyMap()
+) {
+    val favouriteEvents
+        get() = tags[TAG_FAVOURITE].orEmpty()
+
+    val hiddenEvents
+        get() = tags[TAG_HIDDEN].orEmpty()
+
+    fun tagEvent(eventId: String, info: TaggedEventInfo, tag: String) {
+        val taggedEvents = tags[tag].orEmpty().plus(eventId to info)
+        tags = tags.plus(tag to taggedEvents)
+    }
+
+    fun untagEvent(eventId: String, tag: String) {
+        val taggedEvents = tags[tag]?.minus(eventId).orEmpty()
+        tags = tags.plus(tag to taggedEvents)
+    }
+
+    companion object {
+        const val TAG_FAVOURITE = "m.favourite"
+        const val TAG_HIDDEN = "m.hidden"
+    }
+}
+
+@JsonClass(generateAdapter = true)
+data class TaggedEventInfo(
+        @Json(name = "keywords")
+        val keywords: List<String>? = null,
+
+        @Json(name = "origin_server_ts")
+        val originServerTs: Long? = null,
+
+        @Json(name = "tagged_at")
+        val taggedAt: Long? = null
+)
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 2744b5129e0621c0d399d44434851b7da8b2f716..71823cd4585ae113c996af427a27e2e29b321f65 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
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,738 +16,338 @@
 
 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.RealmQuery
-import io.realm.RealmResults
-import io.realm.Sort
-import kotlinx.coroutines.runBlocking
-import org.matrix.android.sdk.api.MatrixCallback
-import org.matrix.android.sdk.api.extensions.orFalse
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.sample
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okhttp3.internal.closeQuietly
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.extensions.tryOrNull
-import org.matrix.android.sdk.api.session.events.model.EventType
-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.api.util.CancelableBag
-import org.matrix.android.sdk.internal.database.RealmSessionProvider
-import org.matrix.android.sdk.internal.database.mapper.EventMapper
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
-import org.matrix.android.sdk.internal.database.model.ChunkEntity
-import org.matrix.android.sdk.internal.database.model.RoomEntity
-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.findAllInRoomWithSendStates
-import org.matrix.android.sdk.internal.database.query.where
-import org.matrix.android.sdk.internal.database.query.whereRoomId
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
 import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
-import org.matrix.android.sdk.internal.task.TaskExecutor
-import org.matrix.android.sdk.internal.task.configureWith
-import org.matrix.android.sdk.internal.util.Debouncer
+import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer
 import org.matrix.android.sdk.internal.util.createBackgroundHandler
-import org.matrix.android.sdk.internal.util.createUIHandler
 import timber.log.Timber
-import java.util.Collections
 import java.util.UUID
 import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicReference
-import kotlin.math.max
-
-private const val MIN_FETCHING_COUNT = 30
-
-internal class DefaultTimeline(
-        private val roomId: String,
-        private var initialEventId: String? = null,
-        private val realmConfiguration: RealmConfiguration,
-        private val taskExecutor: TaskExecutor,
-        private val contextOfEventTask: GetContextOfEventTask,
-        private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
-        private val paginationTask: PaginationTask,
-        private val timelineEventMapper: TimelineEventMapper,
-        private val settings: TimelineSettings,
-        private val timelineInput: TimelineInput,
-        private val eventDecryptor: TimelineEventDecryptor,
-        private val realmSessionProvider: RealmSessionProvider,
-        private val loadRoomMembersTask: LoadRoomMembersTask,
-        private val threadsAwarenessHandler: ThreadsAwarenessHandler,
-        private val readReceiptHandler: ReadReceiptHandler
-) : Timeline,
-        TimelineInput.Listener,
-        UIEchoManager.Listener {
+
+internal class DefaultTimeline(private val roomId: String,
+                               private val initialEventId: String?,
+                               private val realmConfiguration: RealmConfiguration,
+                               private val loadRoomMembersTask: LoadRoomMembersTask,
+                               private val readReceiptHandler: ReadReceiptHandler,
+                               private val settings: TimelineSettings,
+                               private val coroutineDispatchers: MatrixCoroutineDispatchers,
+                               paginationTask: PaginationTask,
+                               getEventTask: GetContextOfEventTask,
+                               fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
+                               timelineEventMapper: TimelineEventMapper,
+                               timelineInput: TimelineInput,
+                               threadsAwarenessHandler: ThreadsAwarenessHandler,
+                               eventDecryptor: TimelineEventDecryptor) : Timeline {
 
     companion object {
-        val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
+        val BACKGROUND_HANDLER = createBackgroundHandler("DefaultTimeline_Thread")
     }
 
-    private val listeners = CopyOnWriteArrayList<Timeline.Listener>()
-    private val isStarted = AtomicBoolean(false)
-    private val isReady = AtomicBoolean(false)
-    private val mainHandler = createUIHandler()
-    private val backgroundRealm = AtomicReference<Realm>()
-    private val cancelableBag = CancelableBag()
-    private val debouncer = Debouncer(mainHandler)
-
-    private lateinit var timelineEvents: RealmResults<TimelineEventEntity>
-    private lateinit var sendingEvents: RealmResults<TimelineEventEntity>
-
-    private var prevDisplayIndex: Int? = null
-    private var nextDisplayIndex: Int? = null
-
-    private val uiEchoManager = UIEchoManager(settings, this)
-
-    private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
-    private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
-    private val backwardsState = AtomicReference(TimelineState())
-    private val forwardsState = AtomicReference(TimelineState())
-
     override val timelineID = UUID.randomUUID().toString()
 
-    override val isLive
-        get() = !hasMoreToLoad(Timeline.Direction.FORWARDS)
-
-    private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet ->
-        if (!results.isLoaded || !results.isValid) {
-            return@OrderedRealmCollectionChangeListener
-        }
-        Timber.v("## SendEvent: [${System.currentTimeMillis()}] DB update for room $roomId")
-        handleUpdates(results, changeSet)
-    }
+    private val listeners = CopyOnWriteArrayList<Timeline.Listener>()
+    private val isStarted = AtomicBoolean(false)
+    private val forwardState = AtomicReference(Timeline.PaginationState())
+    private val backwardState = AtomicReference(Timeline.PaginationState())
 
-    // Public methods ******************************************************************************
+    private val backgroundRealm = AtomicReference<Realm>()
+    private val timelineDispatcher = BACKGROUND_HANDLER.asCoroutineDispatcher()
+    private val timelineScope = CoroutineScope(SupervisorJob() + timelineDispatcher)
+    private val sequencer = SemaphoreCoroutineSequencer()
+    private val postSnapshotSignalFlow = MutableSharedFlow<Unit>(0)
+
+    private val strategyDependencies = LoadTimelineStrategy.Dependencies(
+            timelineSettings = settings,
+            realm = backgroundRealm,
+            eventDecryptor = eventDecryptor,
+            paginationTask = paginationTask,
+            fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
+            getContextOfEventTask = getEventTask,
+            timelineInput = timelineInput,
+            timelineEventMapper = timelineEventMapper,
+            threadsAwarenessHandler = threadsAwarenessHandler,
+            onEventsUpdated = this::sendSignalToPostSnapshot,
+            onLimitedTimeline = this::onLimitedTimeline,
+            onNewTimelineEvents = this::onNewTimelineEvents
+    )
+
+    private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live)
+
+    override val isLive: Boolean
+        get() = !getPaginationState(Timeline.Direction.FORWARDS).hasMoreToLoad
 
-    override fun paginate(direction: Timeline.Direction, count: Int) {
-        BACKGROUND_HANDLER.post {
-            if (!canPaginate(direction)) {
-                return@post
-            }
-            Timber.v("Paginate $direction of $count items")
-            val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
-            val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count)
-            if (shouldPostSnapshot) {
-                postSnapshot()
+    override fun addListener(listener: Timeline.Listener): Boolean {
+        listeners.add(listener)
+        timelineScope.launch {
+            val snapshot = strategy.buildSnapshot()
+            withContext(coroutineDispatchers.main) {
+                tryOrNull { listener.onTimelineUpdated(snapshot) }
             }
         }
+        return true
     }
 
-    override fun pendingEventCount(): Int {
-        return realmSessionProvider.withRealm {
-            RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0
-        }
+    override fun removeListener(listener: Timeline.Listener): Boolean {
+        return listeners.remove(listener)
     }
 
-    override fun failedToDeliverEventCount(): Int {
-        return realmSessionProvider.withRealm {
-            TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count()
-        }
+    override fun removeAllListeners() {
+        listeners.clear()
     }
 
     override fun start() {
-        if (isStarted.compareAndSet(false, true)) {
-            Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
-            timelineInput.listeners.add(this)
-            BACKGROUND_HANDLER.post {
-                eventDecryptor.start()
-                val realm = Realm.getInstance(realmConfiguration)
-                backgroundRealm.set(realm)
-
-                val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
-                        ?: throw IllegalStateException("Can't open a timeline without a room")
-
-                // We don't want to filter here because some sending events that are not displayed
-                // are still used for ui echo (relation like reaction)
-                sendingEvents = roomEntity.sendingTimelineEvents.where()/*.filterEventsWithSettings()*/.findAll()
-                sendingEvents.addChangeListener { events ->
-                    uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
+        timelineScope.launch {
+            loadRoomMembersIfNeeded()
+        }
+        timelineScope.launch {
+            sequencer.post {
+                if (isStarted.compareAndSet(false, true)) {
+                    val realm = Realm.getInstance(realmConfiguration)
+                    ensureReadReceiptAreLoaded(realm)
+                    backgroundRealm.set(realm)
+                    listenToPostSnapshotSignals()
+                    openAround(initialEventId)
                     postSnapshot()
                 }
-
-                timelineEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
-                timelineEvents.addChangeListener(eventsChangeListener)
-                handleInitialLoad()
-                loadRoomMembersTask
-                        .configureWith(LoadRoomMembersTask.Params(roomId))
-                        .executeBy(taskExecutor)
-
-                // Ensure ReadReceipt from init sync are loaded
-                ensureReadReceiptAreLoaded(realm)
-
-                isReady.set(true)
             }
         }
     }
 
-    private fun ensureReadReceiptAreLoaded(realm: Realm) {
-        readReceiptHandler.getContentFromInitSync(roomId)
-                ?.also {
-                    Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId")
-                }
-                ?.let { readReceiptContent ->
-                    realm.executeTransactionAsync {
-                        readReceiptHandler.handle(it, roomId, readReceiptContent, false, null)
-                        readReceiptHandler.onContentFromInitSyncHandled(roomId)
-                    }
-                }
-    }
-
     override fun dispose() {
-        if (isStarted.compareAndSet(true, false)) {
-            isReady.set(false)
-            timelineInput.listeners.remove(this)
-            Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
-            cancelableBag.cancel()
-            BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
-            BACKGROUND_HANDLER.post {
-                if (this::sendingEvents.isInitialized) {
-                    sendingEvents.removeAllChangeListeners()
-                }
-                if (this::timelineEvents.isInitialized) {
-                    timelineEvents.removeAllChangeListeners()
+        timelineScope.coroutineContext.cancelChildren()
+        timelineScope.launch {
+            sequencer.post {
+                if (isStarted.compareAndSet(true, false)) {
+                    strategy.onStop()
+                    backgroundRealm.get().closeQuietly()
                 }
-                clearAllValues()
-                backgroundRealm.getAndSet(null).also {
-                    it?.close()
-                }
-                eventDecryptor.destroy()
             }
         }
     }
 
     override fun restartWithEventId(eventId: String?) {
-        dispose()
-        initialEventId = eventId
-        start()
-        postSnapshot()
-    }
-
-    override fun getTimelineEventAtIndex(index: Int): TimelineEvent? {
-        return builtEvents.getOrNull(index)
-    }
-
-    override fun getIndexOfEvent(eventId: String?): Int? {
-        return builtEventsIdMap[eventId]
-    }
-
-    override fun getTimelineEventWithId(eventId: String?): TimelineEvent? {
-        return builtEventsIdMap[eventId]?.let {
-            getTimelineEventAtIndex(it)
-        }
-    }
-
-    override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
-        return hasMoreInCache(direction) || !hasReachedEnd(direction)
-    }
-
-    override fun addListener(listener: Timeline.Listener): Boolean {
-        if (listeners.contains(listener)) {
-            return false
-        }
-        return listeners.add(listener).also {
+        timelineScope.launch {
+            openAround(eventId)
             postSnapshot()
         }
     }
 
-    override fun removeListener(listener: Timeline.Listener): Boolean {
-        return listeners.remove(listener)
-    }
-
-    override fun removeAllListeners() {
-        listeners.clear()
+    override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
+        return getPaginationState(direction).hasMoreToLoad
     }
 
-    override fun onNewTimelineEvents(roomId: String, eventIds: List<String>) {
-        if (isLive && this.roomId == roomId) {
-            listeners.forEach {
-                it.onNewTimelineEvents(eventIds)
+    override fun paginate(direction: Timeline.Direction, count: Int) {
+        timelineScope.launch {
+            val postSnapshot = loadMore(count, direction, fetchOnServerIfNeeded = true)
+            if (postSnapshot) {
+                postSnapshot()
             }
         }
     }
 
-    override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) {
-        if (roomId != this.roomId || !isLive) return
-        uiEchoManager.onLocalEchoCreated(timelineEvent)
-        listeners.forEach {
-            tryOrNull {
-                it.onNewTimelineEvents(listOf(timelineEvent.eventId))
-            }
+    override suspend fun awaitPaginate(direction: Timeline.Direction, count: Int): List<TimelineEvent> {
+        withContext(timelineDispatcher) {
+            loadMore(count, direction, fetchOnServerIfNeeded = true)
         }
-        postSnapshot()
+        return getSnapshot()
     }
 
-    override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) {
-        if (roomId != this.roomId || !isLive) return
-        if (uiEchoManager.onSendStateUpdated(eventId, sendState)) {
-            postSnapshot()
-        }
+    override fun getSnapshot(): List<TimelineEvent> {
+        return strategy.buildSnapshot()
     }
 
-    override fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean {
-        return tryOrNull {
-            builtEventsIdMap[eventId]?.let { builtIndex ->
-                // Update the relation of existing event
-                builtEvents[builtIndex]?.let { te ->
-                    val rebuiltEvent = builder(te)
-                    // If rebuilt event is filtered its returned as null and should be removed.
-                    if (rebuiltEvent == null) {
-                        builtEventsIdMap.remove(eventId)
-                        builtEventsIdMap.entries.filter { it.value > builtIndex }.forEach { it.setValue(it.value - 1) }
-                        builtEvents.removeAt(builtIndex)
-                    } else {
-                        builtEvents[builtIndex] = rebuiltEvent
-                    }
-                    true
-                }
-            }
-        } ?: false
+    override fun getIndexOfEvent(eventId: String?): Int? {
+        if (eventId == null) return null
+        return strategy.getBuiltEventIndex(eventId)
     }
 
-// Private methods *****************************************************************************
-
-    private fun hasMoreInCache(direction: Timeline.Direction) = getState(direction).hasMoreInCache
-
-    private fun hasReachedEnd(direction: Timeline.Direction) = getState(direction).hasReachedEnd
-
-    private fun updateLoadingStates(results: RealmResults<TimelineEventEntity>) {
-        val lastCacheEvent = results.lastOrNull()
-        val firstCacheEvent = results.firstOrNull()
-        val chunkEntity = getLiveChunk()
-
-        updateState(Timeline.Direction.FORWARDS) {
-            it.copy(
-                    hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId),
-                    hasReachedEnd = chunkEntity?.isLastForward ?: false
-            )
-        }
-        updateState(Timeline.Direction.BACKWARDS) {
-            it.copy(
-                    hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId),
-                    hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE
-            )
-        }
+    override fun getPaginationState(direction: Timeline.Direction): Timeline.PaginationState {
+        return if (direction == Timeline.Direction.BACKWARDS) {
+            backwardState
+        } else {
+            forwardState
+        }.get()
     }
 
-    /**
-     * This has to be called on TimelineThread as it accesses realm live results
-     * @return true if createSnapshot should be posted
-     */
-    private fun paginateInternal(startDisplayIndex: Int?,
-                                 direction: Timeline.Direction,
-                                 count: Int): Boolean {
-        if (count == 0) {
+    private suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean): Boolean {
+        val baseLogMessage = "loadMore(count: $count, direction: $direction, roomId: $roomId, fetchOnServer: $fetchOnServerIfNeeded)"
+        Timber.v("$baseLogMessage started")
+        if (!isStarted.get()) {
+            throw IllegalStateException("You should call start before using timeline")
+        }
+        val currentState = getPaginationState(direction)
+        if (!currentState.hasMoreToLoad) {
+            Timber.v("$baseLogMessage : nothing more to load")
             return false
         }
-        updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) }
-        val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
-        val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)
-        if (shouldFetchMore) {
-            val newRequestedCount = count - builtCount
-            updateState(direction) { it.copy(requestedPaginationCount = newRequestedCount) }
-            val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount)
-            executePaginationTask(direction, fetchingCount)
-        } else {
-            updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
+        if (currentState.loading) {
+            Timber.v("$baseLogMessage : already loading")
+            return false
         }
-        return !shouldFetchMore
-    }
-
-    private fun createSnapshot(): List<TimelineEvent> {
-        return buildSendingEvents() + builtEvents.toList()
-    }
-
-    private fun buildSendingEvents(): List<TimelineEvent> {
-        val builtSendingEvents = mutableListOf<TimelineEvent>()
-        if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
-            uiEchoManager.getInMemorySendingEvents()
-                    .updateWithUiEchoInto(builtSendingEvents)
-            sendingEvents
-                    .filter { timelineEvent ->
-                        builtSendingEvents.none { it.eventId == timelineEvent.eventId }
-                    }
-                    .map { timelineEventMapper.map(it) }
-                    .updateWithUiEchoInto(builtSendingEvents)
+        updateState(direction) {
+            it.copy(loading = true)
         }
-        return builtSendingEvents
-    }
-
-    private fun List<TimelineEvent>.updateWithUiEchoInto(target: MutableList<TimelineEvent>) {
-        target.addAll(
-                // Get most up to date send state (in memory)
-                map { uiEchoManager.updateSentStateWithUiEcho(it) }
-        )
-    }
-
-    private fun canPaginate(direction: Timeline.Direction): Boolean {
-        return isReady.get() && !getState(direction).isPaginating && hasMoreToLoad(direction)
-    }
-
-    private fun getState(direction: Timeline.Direction): TimelineState {
-        return when (direction) {
-            Timeline.Direction.FORWARDS  -> forwardsState.get()
-            Timeline.Direction.BACKWARDS -> backwardsState.get()
+        val loadMoreResult = strategy.loadMore(count, direction, fetchOnServerIfNeeded)
+        Timber.v("$baseLogMessage: result $loadMoreResult")
+        val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END
+        updateState(direction) {
+            it.copy(loading = false, hasMoreToLoad = hasMoreToLoad)
         }
+        return true
     }
 
-    private fun updateState(direction: Timeline.Direction, update: (TimelineState) -> TimelineState) {
-        val stateReference = when (direction) {
-            Timeline.Direction.FORWARDS  -> forwardsState
-            Timeline.Direction.BACKWARDS -> backwardsState
+    private suspend fun openAround(eventId: String?) = withContext(timelineDispatcher) {
+        val baseLogMessage = "openAround(eventId: $eventId)"
+        Timber.v("$baseLogMessage started")
+        if (!isStarted.get()) {
+            throw IllegalStateException("You should call start before using timeline")
         }
-        val currentValue = stateReference.get()
-        val newValue = update(currentValue)
-        stateReference.set(newValue)
-    }
-
-    /**
-     * This has to be called on TimelineThread as it accesses realm live results
-     */
-    private fun handleInitialLoad() {
-        var shouldFetchInitialEvent = false
-        val currentInitialEventId = initialEventId
-        val initialDisplayIndex = if (currentInitialEventId == null) {
-            timelineEvents.firstOrNull()?.displayIndex
+        strategy.onStop()
+        strategy = if (eventId == null) {
+            buildStrategy(LoadTimelineStrategy.Mode.Live)
         } else {
-            val initialEvent = timelineEvents.where()
-                    .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId)
-                    .findFirst()
+            buildStrategy(LoadTimelineStrategy.Mode.Permalink(eventId))
+        }
+        initPaginationStates(eventId)
+        strategy.onStart()
+        loadMore(
+                count = strategyDependencies.timelineSettings.initialSize,
+                direction = Timeline.Direction.BACKWARDS,
+                fetchOnServerIfNeeded = false
+        )
+        Timber.v("$baseLogMessage finished")
+    }
 
-            shouldFetchInitialEvent = initialEvent == null
-            initialEvent?.displayIndex
+    private fun initPaginationStates(eventId: String?) {
+        updateState(Timeline.Direction.FORWARDS) {
+            it.copy(loading = false, hasMoreToLoad = eventId != null)
         }
-        prevDisplayIndex = initialDisplayIndex
-        nextDisplayIndex = initialDisplayIndex
-        if (currentInitialEventId != null && shouldFetchInitialEvent) {
-            fetchEvent(currentInitialEventId)
-        } else {
-            val count = timelineEvents.size.coerceAtMost(settings.initialSize)
-            if (initialEventId == null) {
-                paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
-            } else {
-                paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, (count / 2).coerceAtLeast(1))
-                paginateInternal(initialDisplayIndex?.minus(1), Timeline.Direction.BACKWARDS, (count / 2).coerceAtLeast(1))
-            }
+        updateState(Timeline.Direction.BACKWARDS) {
+            it.copy(loading = false, hasMoreToLoad = true)
         }
-        postSnapshot()
     }
 
-    /**
-     * This has to be called on TimelineThread as it accesses realm live results
-     */
-    private fun handleUpdates(results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
-        // If changeSet has deletion we are having a gap, so we clear everything
-        if (changeSet.deletionRanges.isNotEmpty()) {
-            clearAllValues()
-        }
-        var postSnapshot = false
-        changeSet.insertionRanges.forEach { range ->
-            val (startDisplayIndex, direction) = if (range.startIndex == 0) {
-                Pair(results[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
-            } else {
-                Pair(results[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS)
-            }
-            val state = getState(direction)
-            if (state.isPaginating) {
-                // We are getting new items from pagination
-                postSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedPaginationCount)
+    private fun sendSignalToPostSnapshot(withThrottling: Boolean) {
+        timelineScope.launch {
+            if (withThrottling) {
+                postSnapshotSignalFlow.emit(Unit)
             } else {
-                // We are getting new items from sync
-                buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
-                postSnapshot = true
-            }
-        }
-        changeSet.changes.forEach { index ->
-            val eventEntity = results[index]
-            eventEntity?.eventId?.let { eventId ->
-                postSnapshot = rebuildEvent(eventId) {
-                    buildTimelineEvent(eventEntity)
-                } || postSnapshot
+                postSnapshot()
             }
         }
-        if (postSnapshot) {
-            postSnapshot()
-        }
     }
 
-    /**
-     * This has to be called on TimelineThread as it accesses realm live results
-     */
-    private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
-        val currentChunk = getLiveChunk()
-        val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken
-        if (token == null) {
-            if (direction == Timeline.Direction.BACKWARDS ||
-                    (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse())) {
-                // We are in the case where event exists, but we do not know the token.
-                // Fetch (again) the last event to get a token
-                val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) {
-                    timelineEvents.firstOrNull()?.eventId
-                } else {
-                    timelineEvents.lastOrNull()?.eventId
-                }
-                if (lastKnownEventId == null) {
-                    updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
-                } else {
-                    val params = FetchTokenAndPaginateTask.Params(
-                            roomId = roomId,
-                            limit = limit,
-                            direction = direction.toPaginationDirection(),
-                            lastKnownEventId = lastKnownEventId
-                    )
-                    cancelableBag += fetchTokenAndPaginateTask
-                            .configureWith(params) {
-                                this.callback = createPaginationCallback(limit, direction)
-                            }
-                            .executeBy(taskExecutor)
+    @Suppress("EXPERIMENTAL_API_USAGE")
+    private fun listenToPostSnapshotSignals() {
+        postSnapshotSignalFlow
+                .sample(150)
+                .onEach {
+                    postSnapshot()
                 }
-            } else {
-                updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
-            }
-        } else {
-            val params = PaginationTask.Params(
-                    roomId = roomId,
-                    from = token,
-                    direction = direction.toPaginationDirection(),
-                    limit = limit
-            )
-            Timber.v("Should fetch $limit items $direction")
-            cancelableBag += paginationTask
-                    .configureWith(params) {
-                        this.callback = createPaginationCallback(limit, direction)
-                    }
-                    .executeBy(taskExecutor)
-        }
+                .launchIn(timelineScope)
     }
 
-    // For debug purpose only
-    private fun dumpAndLogChunks() {
-        val liveChunk = getLiveChunk()
-        Timber.w("Live chunk: $liveChunk")
-
-        Realm.getInstance(realmConfiguration).use { realm ->
-            ChunkEntity.where(realm, roomId).findAll()
-                    .also { Timber.w("Found ${it.size} chunks") }
-                    .forEach {
-                        Timber.w("")
-                        Timber.w("ChunkEntity: $it")
-                        Timber.w("prevToken: ${it.prevToken}")
-                        Timber.w("nextToken: ${it.nextToken}")
-                        Timber.w("isLastBackward: ${it.isLastBackward}")
-                        Timber.w("isLastForward: ${it.isLastForward}")
-                        it.timelineEvents.forEach { tle ->
-                            Timber.w("   TLE: ${tle.root?.content}")
-                        }
-                    }
+    private fun onLimitedTimeline() {
+        timelineScope.launch {
+            initPaginationStates(null)
+            loadMore(settings.initialSize, Timeline.Direction.BACKWARDS, false)
+            postSnapshot()
         }
     }
 
-    /**
-     * This has to be called on TimelineThread as it accesses realm live results
-     */
-    private fun getTokenLive(direction: Timeline.Direction): String? {
-        val chunkEntity = getLiveChunk() ?: return null
-        return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
-    }
-
-    /**
-     * This has to be called on TimelineThread as it accesses realm live results
-     * Return the current Chunk
-     */
-    private fun getLiveChunk(): ChunkEntity? {
-        return timelineEvents.firstOrNull()?.chunk?.firstOrNull()
-    }
-
-    /**
-     * This has to be called on TimelineThread as it accesses realm live results
-     * @return the number of items who have been added
-     */
-    private fun buildTimelineEvents(startDisplayIndex: Int?,
-                                    direction: Timeline.Direction,
-                                    count: Long): Int {
-        if (count < 1 || startDisplayIndex == null) {
-            return 0
-        }
-        val start = System.currentTimeMillis()
-        val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
-        if (offsetResults.isEmpty()) {
-            return 0
-        }
-        val offsetIndex = offsetResults.last()!!.displayIndex
-        if (direction == Timeline.Direction.BACKWARDS) {
-            prevDisplayIndex = offsetIndex - 1
-        } else {
-            nextDisplayIndex = offsetIndex + 1
-        }
-
-        // Prerequisite to in order for the ThreadsAwarenessHandler to work properly
-        fetchRootThreadEventsIfNeeded(offsetResults)
-
-        offsetResults.forEach { eventEntity ->
-
-            val timelineEvent = buildTimelineEvent(eventEntity)
-            val transactionId = timelineEvent.root.unsignedData?.transactionId
-            uiEchoManager.onSyncedEvent(transactionId)
-
-            if (timelineEvent.isEncrypted() &&
-                    timelineEvent.root.mxDecryptionResult == null) {
-                timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineID)) }
+    private suspend fun postSnapshot() {
+        val snapshot = strategy.buildSnapshot()
+        Timber.v("Post snapshot of ${snapshot.size} events")
+        withContext(coroutineDispatchers.main) {
+            listeners.forEach {
+                tryOrNull { it.onTimelineUpdated(snapshot) }
             }
-
-            val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
-            builtEvents.add(position, timelineEvent)
-            // Need to shift :/
-            builtEventsIdMap.entries.filter { it.value >= position }.forEach { it.setValue(it.value + 1) }
-            builtEventsIdMap[eventEntity.eventId] = position
         }
-        val time = System.currentTimeMillis() - start
-        Timber.v("Built ${offsetResults.size} items from db in $time ms")
-        // For the case where wo reach the lastForward chunk
-        updateLoadingStates(timelineEvents)
-        return offsetResults.size
-    }
-
-    /**
-     * This function is responsible to fetch and store the root event of a thread event
-     * in order to be able to display the event to the user appropriately
-     */
-    private fun fetchRootThreadEventsIfNeeded(offsetResults: RealmResults<TimelineEventEntity>) = runBlocking {
-        val eventEntityList = offsetResults
-                .mapNotNull {
-                    it?.root
-                }.map {
-                    EventMapper.map(it)
-                }
-        threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
     }
 
-    private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent {
-        return timelineEventMapper.map(
-                timelineEventEntity = eventEntity,
-                buildReadReceipts = settings.buildReadReceipts
-        ).let { timelineEvent ->
-            // eventually enhance with ui echo?
-            uiEchoManager.decorateEventWithReactionUiEcho(timelineEvent) ?: timelineEvent
+    private fun onNewTimelineEvents(eventIds: List<String>) {
+        timelineScope.launch(coroutineDispatchers.main) {
+            listeners.forEach {
+                tryOrNull { it.onNewTimelineEvents(eventIds) }
+            }
         }
     }
 
-    /**
-     * This has to be called on TimelineThread as it accesses realm live results
-     */
-    private fun getOffsetResults(startDisplayIndex: Int,
-                                 direction: Timeline.Direction,
-                                 count: Long): RealmResults<TimelineEventEntity> {
-        val offsetQuery = timelineEvents.where()
-        if (direction == Timeline.Direction.BACKWARDS) {
-            offsetQuery
-                    .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
-                    .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
-        } else {
-            offsetQuery
-                    .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
-                    .greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
+    private fun updateState(direction: Timeline.Direction, update: (Timeline.PaginationState) -> Timeline.PaginationState) {
+        val stateReference = when (direction) {
+            Timeline.Direction.FORWARDS  -> forwardState
+            Timeline.Direction.BACKWARDS -> backwardState
         }
-        return offsetQuery
-                .limit(count)
-                .findAll()
-    }
-
-    private fun buildEventQuery(realm: Realm): RealmQuery<TimelineEventEntity> {
-        return if (initialEventId == null) {
-            TimelineEventEntity
-                    .whereRoomId(realm, roomId = roomId)
-                    .equalTo(TimelineEventEntityFields.CHUNK.IS_LAST_FORWARD, true)
-        } else {
-            TimelineEventEntity
-                    .whereRoomId(realm, roomId = roomId)
-                    .`in`("${TimelineEventEntityFields.CHUNK.TIMELINE_EVENTS}.${TimelineEventEntityFields.EVENT_ID}", arrayOf(initialEventId))
+        val currentValue = stateReference.get()
+        val newValue = update(currentValue)
+        stateReference.set(newValue)
+        if (newValue != currentValue) {
+            postPaginationState(direction, newValue)
         }
     }
 
-    private fun fetchEvent(eventId: String) {
-        val params = GetContextOfEventTask.Params(roomId, eventId)
-        cancelableBag += contextOfEventTask.configureWith(params) {
-            callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
-                override fun onSuccess(data: TokenChunkEventPersistor.Result) {
-                    postSnapshot()
-                }
-
-                override fun onFailure(failure: Throwable) {
-                    postFailure(failure)
-                }
+    private fun postPaginationState(direction: Timeline.Direction, state: Timeline.PaginationState) {
+        timelineScope.launch(coroutineDispatchers.main) {
+            Timber.v("Post $direction pagination state: $state ")
+            listeners.forEach {
+                tryOrNull { it.onStateUpdated(direction, state) }
             }
         }
-                .executeBy(taskExecutor)
     }
 
-    private fun postSnapshot() {
-        BACKGROUND_HANDLER.post {
-            if (isReady.get().not()) {
-                return@post
-            }
-            updateLoadingStates(timelineEvents)
-            val snapshot = createSnapshot()
-            val runnable = Runnable {
-                listeners.forEach {
-                    it.onTimelineUpdated(snapshot)
-                }
-            }
-            debouncer.debounce("post_snapshot", runnable, 1)
-        }
+    private fun buildStrategy(mode: LoadTimelineStrategy.Mode): LoadTimelineStrategy {
+        return LoadTimelineStrategy(
+                roomId = roomId,
+                timelineId = timelineID,
+                mode = mode,
+                dependencies = strategyDependencies
+        )
     }
 
-    private fun postFailure(throwable: Throwable) {
-        if (isReady.get().not()) {
-            return
+    private suspend fun loadRoomMembersIfNeeded() {
+        val loadRoomMembersParam = LoadRoomMembersTask.Params(roomId)
+        try {
+            loadRoomMembersTask.execute(loadRoomMembersParam)
+        } catch (failure: Throwable) {
+            Timber.v("Failed to load room members. Retry in 10s.")
+            delay(10_000L)
+            loadRoomMembersIfNeeded()
         }
-        val runnable = Runnable {
-            listeners.forEach {
-                it.onTimelineFailure(throwable)
-            }
-        }
-        mainHandler.post(runnable)
     }
 
-    private fun clearAllValues() {
-        prevDisplayIndex = null
-        nextDisplayIndex = null
-        builtEvents.clear()
-        builtEventsIdMap.clear()
-        backwardsState.set(TimelineState())
-        forwardsState.set(TimelineState())
-    }
-
-    private fun createPaginationCallback(limit: Int, direction: Timeline.Direction): MatrixCallback<TokenChunkEventPersistor.Result> {
-        return object : MatrixCallback<TokenChunkEventPersistor.Result> {
-            override fun onSuccess(data: TokenChunkEventPersistor.Result) {
-                when (data) {
-                    TokenChunkEventPersistor.Result.SUCCESS           -> {
-                        Timber.v("Success fetching $limit items $direction from pagination request")
-                    }
-                    TokenChunkEventPersistor.Result.REACHED_END       -> {
-                        postSnapshot()
+    private fun ensureReadReceiptAreLoaded(realm: Realm) {
+        readReceiptHandler.getContentFromInitSync(roomId)
+                ?.also {
+                    Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId")
+                }
+                ?.let { readReceiptContent ->
+                    realm.executeTransactionAsync {
+                        readReceiptHandler.handle(it, roomId, readReceiptContent, false, null)
+                        readReceiptHandler.onContentFromInitSyncHandled(roomId)
                     }
-                    TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
-                        // Database won't be updated, so we force pagination request
-                        BACKGROUND_HANDLER.post {
-                            executePaginationTask(direction, limit)
-                        }
                 }
-            }
-
-            override fun onFailure(failure: Throwable) {
-                updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
-                postSnapshot()
-                Timber.v("Failure fetching $limit items $direction from pagination request")
-            }
-        }
-    }
-
-    // Extension methods ***************************************************************************
-
-    private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
-        return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
     }
 }
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 75e7e774df47ff23bfba5dc69bc38a245d7b1ad8..126374b430c48165916bf193b601bed7bc922bc3 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
@@ -23,6 +23,7 @@ import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import io.realm.Sort
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.session.events.model.isImageMessage
 import org.matrix.android.sdk.api.session.events.model.isVideoMessage
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
@@ -54,7 +55,8 @@ internal class DefaultTimelineService @AssistedInject constructor(
         private val timelineEventMapper: TimelineEventMapper,
         private val loadRoomMembersTask: LoadRoomMembersTask,
         private val threadsAwarenessHandler: ThreadsAwarenessHandler,
-        private val readReceiptHandler: ReadReceiptHandler
+        private val readReceiptHandler: ReadReceiptHandler,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers
 ) : TimelineService {
 
     @AssistedFactory
@@ -66,19 +68,18 @@ internal class DefaultTimelineService @AssistedInject constructor(
         return DefaultTimeline(
                 roomId = roomId,
                 initialEventId = eventId,
+                settings = settings,
                 realmConfiguration = monarchy.realmConfiguration,
-                taskExecutor = taskExecutor,
-                contextOfEventTask = contextOfEventTask,
+                coroutineDispatchers = coroutineDispatchers,
                 paginationTask = paginationTask,
                 timelineEventMapper = timelineEventMapper,
-                settings = settings,
                 timelineInput = timelineInput,
                 eventDecryptor = eventDecryptor,
                 fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
-                realmSessionProvider = realmSessionProvider,
                 loadRoomMembersTask = loadRoomMembersTask,
-                threadsAwarenessHandler = threadsAwarenessHandler,
-                readReceiptHandler = readReceiptHandler
+                readReceiptHandler = readReceiptHandler,
+                getEventTask = contextOfEventTask,
+                threadsAwarenessHandler = threadsAwarenessHandler
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt
similarity index 76%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt
index 0143d9bab3d21498b03c9bb1f2f23d6f48609a6d..c419e8325ee5c506479cb71cb8faa104a8b61b8a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt
@@ -16,9 +16,8 @@
 
 package org.matrix.android.sdk.internal.session.room.timeline
 
-internal data class TimelineState(
-        val hasReachedEnd: Boolean = false,
-        val hasMoreInCache: Boolean = true,
-        val isPaginating: Boolean = false,
-        val requestedPaginationCount: Int = 0
-)
+internal enum class LoadMoreResult {
+    REACHED_END,
+    SUCCESS,
+    FAILURE
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..528b564e8b13331b30c186199644b6caa40bd480
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.timeline
+
+import io.realm.OrderedCollectionChangeSet
+import io.realm.OrderedRealmCollectionChangeListener
+import io.realm.Realm
+import io.realm.RealmResults
+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.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.query.findAllIncludingEvents
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * This class is responsible for keeping an instance of chunkEntity and timelineChunk according to the strategy.
+ * There is 2 different mode: Live and Permalink.
+ * In Live, we will query for the live chunk (isLastForward = true).
+ * In Permalink, we will query for the chunk including the eventId we are looking for.
+ * Once we got a ChunkEntity we wrap it with TimelineChunk class so we dispatch any methods for loading data.
+ */
+
+internal class LoadTimelineStrategy(
+        private val roomId: String,
+        private val timelineId: String,
+        private val mode: Mode,
+        private val dependencies: Dependencies) {
+
+    sealed interface Mode {
+        object Live : Mode
+        data class Permalink(val originEventId: String) : Mode
+
+        fun originEventId(): String? {
+            return if (this is Permalink) {
+                originEventId
+            } else {
+                null
+            }
+        }
+    }
+
+    data class Dependencies(
+            val timelineSettings: TimelineSettings,
+            val realm: AtomicReference<Realm>,
+            val eventDecryptor: TimelineEventDecryptor,
+            val paginationTask: PaginationTask,
+            val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
+            val getContextOfEventTask: GetContextOfEventTask,
+            val timelineInput: TimelineInput,
+            val timelineEventMapper: TimelineEventMapper,
+            val threadsAwarenessHandler: ThreadsAwarenessHandler,
+            val onEventsUpdated: (Boolean) -> Unit,
+            val onLimitedTimeline: () -> Unit,
+            val onNewTimelineEvents: (List<String>) -> Unit
+    )
+
+    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.
+        val shouldRebuildChunk = changeSet.insertions.isNotEmpty()
+        if (shouldRebuildChunk) {
+            timelineChunk?.close(closeNext = true, closePrev = true)
+            timelineChunk = chunkEntity?.createTimelineChunk()
+            // If we are waiting for a result of get context, post completion
+            getContextLatch?.complete(Unit)
+            // If we have a gap, just tell the timeline about it.
+            if (timelineChunk?.hasReachedLastForward().orFalse()) {
+                dependencies.onLimitedTimeline()
+            }
+        }
+    }
+
+    private val uiEchoManagerListener = object : UIEchoManager.Listener {
+        override fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean {
+            return timelineChunk?.rebuildEvent(eventId, builder, searchInNext = true, searchInPrev = true).orFalse()
+        }
+    }
+
+    private val timelineInputListener = object : TimelineInput.Listener {
+
+        override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) {
+            if (roomId != this@LoadTimelineStrategy.roomId) {
+                return
+            }
+            if (uiEchoManager.onLocalEchoCreated(timelineEvent)) {
+                dependencies.onNewTimelineEvents(listOf(timelineEvent.eventId))
+                dependencies.onEventsUpdated(false)
+            }
+        }
+
+        override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) {
+            if (roomId != this@LoadTimelineStrategy.roomId) {
+                return
+            }
+            if (uiEchoManager.onSendStateUpdated(eventId, sendState)) {
+                dependencies.onEventsUpdated(false)
+            }
+        }
+
+        override fun onNewTimelineEvents(roomId: String, eventIds: List<String>) {
+            if (roomId == this@LoadTimelineStrategy.roomId && hasReachedLastForward()) {
+                dependencies.onNewTimelineEvents(eventIds)
+            }
+        }
+    }
+
+    private val uiEchoManager = UIEchoManager(uiEchoManagerListener)
+    private val sendingEventsDataSource: SendingEventsDataSource = RealmSendingEventsDataSource(
+            roomId = roomId,
+            realm = dependencies.realm,
+            uiEchoManager = uiEchoManager,
+            timelineEventMapper = dependencies.timelineEventMapper,
+            onEventsUpdated = dependencies.onEventsUpdated
+    )
+
+    fun onStart() {
+        dependencies.eventDecryptor.start()
+        dependencies.timelineInput.listeners.add(timelineInputListener)
+        val realm = dependencies.realm.get()
+        sendingEventsDataSource.start()
+        chunkEntity = getChunkEntity(realm).also {
+            it.addChangeListener(chunkEntityListener)
+            timelineChunk = it.createTimelineChunk()
+        }
+    }
+
+    fun onStop() {
+        dependencies.eventDecryptor.destroy()
+        dependencies.timelineInput.listeners.remove(timelineInputListener)
+        chunkEntity?.removeChangeListener(chunkEntityListener)
+        sendingEventsDataSource.stop()
+        timelineChunk?.close(closeNext = true, closePrev = true)
+        getContextLatch?.cancel()
+        chunkEntity = null
+        timelineChunk = null
+    }
+
+    suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
+        if (mode is Mode.Permalink && timelineChunk == null) {
+            val params = GetContextOfEventTask.Params(roomId, mode.originEventId)
+            try {
+                getContextLatch = CompletableDeferred()
+                dependencies.getContextOfEventTask.execute(params)
+                // waits for the query to be fulfilled
+                getContextLatch?.await()
+                getContextLatch = null
+            } catch (failure: Throwable) {
+                return LoadMoreResult.FAILURE
+            }
+        }
+        return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
+    }
+
+    fun getBuiltEventIndex(eventId: String): Int? {
+        return timelineChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = true)
+    }
+
+    fun getBuiltEvent(eventId: String): TimelineEvent? {
+        return timelineChunk?.getBuiltEvent(eventId, searchInNext = true, searchInPrev = true)
+    }
+
+    fun buildSnapshot(): List<TimelineEvent> {
+        return buildSendingEvents() + timelineChunk?.builtItems(includesNext = true, includesPrev = true).orEmpty()
+    }
+
+    private fun buildSendingEvents(): List<TimelineEvent> {
+        return if (hasReachedLastForward()) {
+            sendingEventsDataSource.buildSendingEvents()
+        } else {
+            emptyList()
+        }
+    }
+
+    private fun getChunkEntity(realm: Realm): RealmResults<ChunkEntity> {
+        return if (mode is Mode.Permalink) {
+            ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId))
+        } else {
+            ChunkEntity.where(realm, roomId)
+                    .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
+                    .findAll()
+        }
+    }
+
+    private fun hasReachedLastForward(): Boolean {
+        return timelineChunk?.hasReachedLastForward().orFalse()
+    }
+
+    private fun RealmResults<ChunkEntity>.createTimelineChunk(): TimelineChunk? {
+        return firstOrNull()?.let {
+            return TimelineChunk(
+                    chunkEntity = it,
+                    timelineSettings = dependencies.timelineSettings,
+                    roomId = roomId,
+                    timelineId = timelineId,
+                    eventDecryptor = dependencies.eventDecryptor,
+                    paginationTask = dependencies.paginationTask,
+                    fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask,
+                    timelineEventMapper = dependencies.timelineEventMapper,
+                    uiEchoManager = uiEchoManager,
+                    threadsAwarenessHandler = dependencies.threadsAwarenessHandler,
+                    initialEventId = mode.originEventId(),
+                    onBuiltEvents = dependencies.onEventsUpdated
+            )
+        }
+    }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..a98de1c595ad3df8508e5e15d6f8adfeec323cbd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.timeline
+
+import io.realm.Realm
+import io.realm.RealmChangeListener
+import io.realm.RealmList
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
+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.query.where
+import java.util.concurrent.atomic.AtomicReference
+
+internal interface SendingEventsDataSource {
+    fun start()
+    fun stop()
+    fun buildSendingEvents(): List<TimelineEvent>
+}
+
+internal class RealmSendingEventsDataSource(
+        private val roomId: String,
+        private val realm: AtomicReference<Realm>,
+        private val uiEchoManager: UIEchoManager,
+        private val timelineEventMapper: TimelineEventMapper,
+        private val onEventsUpdated: (Boolean) -> Unit
+) : SendingEventsDataSource {
+
+    private var roomEntity: RoomEntity? = null
+    private var sendingTimelineEvents: RealmList<TimelineEventEntity>? = null
+    private var frozenSendingTimelineEvents: RealmList<TimelineEventEntity>? = null
+
+    private val sendingTimelineEventsListener = RealmChangeListener<RealmList<TimelineEventEntity>> { events ->
+        uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
+        frozenSendingTimelineEvents = sendingTimelineEvents?.freeze()
+        onEventsUpdated(false)
+    }
+
+    override fun start() {
+        val safeRealm = realm.get()
+        roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst()
+        sendingTimelineEvents = roomEntity?.sendingTimelineEvents
+        sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener)
+    }
+
+    override fun stop() {
+        sendingTimelineEvents?.removeChangeListener(sendingTimelineEventsListener)
+        sendingTimelineEvents = null
+        roomEntity = null
+    }
+
+    override fun buildSendingEvents(): List<TimelineEvent> {
+        val builtSendingEvents = mutableListOf<TimelineEvent>()
+        uiEchoManager.getInMemorySendingEvents()
+                .addWithUiEcho(builtSendingEvents)
+        frozenSendingTimelineEvents
+                ?.filter { timelineEvent ->
+                    builtSendingEvents.none { it.eventId == timelineEvent.eventId }
+                }
+                ?.map {
+                    timelineEventMapper.map(it)
+                }?.addWithUiEcho(builtSendingEvents)
+
+        return builtSendingEvents
+    }
+
+    private fun List<TimelineEvent>.addWithUiEcho(target: MutableList<TimelineEvent>) {
+        target.addAll(
+                map { uiEchoManager.updateSentStateWithUiEcho(it) }
+        )
+    }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..14cba2a4b8de751e33f05c9b86fb03ae34037236
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -0,0 +1,479 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.timeline
+
+import io.realm.OrderedCollectionChangeSet
+import io.realm.OrderedRealmCollectionChangeListener
+import io.realm.RealmObjectChangeListener
+import io.realm.RealmQuery
+import io.realm.RealmResults
+import io.realm.Sort
+import kotlinx.coroutines.CompletableDeferred
+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
+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.mapper.EventMapper
+import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
+import org.matrix.android.sdk.internal.database.model.ChunkEntity
+import org.matrix.android.sdk.internal.database.model.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.sync.handler.room.ThreadsAwarenessHandler
+import timber.log.Timber
+import java.util.Collections
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * This is a wrapper around a ChunkEntity in the database.
+ * It does mainly listen to the db timeline events.
+ * It also triggers pagination to the server when needed, or dispatch to the prev or next chunk if any.
+ */
+internal class TimelineChunk(private val chunkEntity: ChunkEntity,
+                             private val timelineSettings: TimelineSettings,
+                             private val roomId: String,
+                             private val timelineId: String,
+                             private val eventDecryptor: TimelineEventDecryptor,
+                             private val paginationTask: PaginationTask,
+                             private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
+                             private val timelineEventMapper: TimelineEventMapper,
+                             private val uiEchoManager: UIEchoManager? = null,
+                             private val threadsAwarenessHandler: ThreadsAwarenessHandler,
+                             private val initialEventId: String?,
+                             private val onBuiltEvents: (Boolean) -> Unit) {
+
+    private val isLastForward = AtomicBoolean(chunkEntity.isLastForward)
+    private val isLastBackward = AtomicBoolean(chunkEntity.isLastBackward)
+    private var prevChunkLatch: CompletableDeferred<Unit>? = null
+    private var nextChunkLatch: CompletableDeferred<Unit>? = null
+
+    private val chunkObjectListener = RealmObjectChangeListener<ChunkEntity> { _, changeSet ->
+        if (changeSet == null) return@RealmObjectChangeListener
+        if (changeSet.isDeleted.orFalse()) {
+            return@RealmObjectChangeListener
+        }
+        Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet.changedFields?.joinToString(",")}")
+        if (changeSet.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD)) {
+            isLastForward.set(chunkEntity.isLastForward)
+        }
+        if (changeSet.isFieldChanged(ChunkEntityFields.IS_LAST_BACKWARD)) {
+            isLastBackward.set(chunkEntity.isLastBackward)
+        }
+        if (changeSet.isFieldChanged(ChunkEntityFields.NEXT_CHUNK.`$`)) {
+            nextChunk = createTimelineChunk(chunkEntity.nextChunk)
+            nextChunkLatch?.complete(Unit)
+        }
+        if (changeSet.isFieldChanged(ChunkEntityFields.PREV_CHUNK.`$`)) {
+            prevChunk = createTimelineChunk(chunkEntity.prevChunk)
+            prevChunkLatch?.complete(Unit)
+        }
+    }
+
+    private val timelineEventsChangeListener =
+            OrderedRealmCollectionChangeListener { results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet ->
+                Timber.v("on timeline events chunk update")
+                val frozenResults = results.freeze()
+                handleDatabaseChangeSet(frozenResults, changeSet)
+            }
+
+    private var timelineEventEntities: RealmResults<TimelineEventEntity> = chunkEntity.sortedTimelineEvents()
+    private val builtEvents: MutableList<TimelineEvent> = Collections.synchronizedList(ArrayList())
+    private val builtEventsIndexes: MutableMap<String, Int> = Collections.synchronizedMap(HashMap<String, Int>())
+
+    private var nextChunk: TimelineChunk? = null
+    private var prevChunk: TimelineChunk? = null
+
+    init {
+        timelineEventEntities.addChangeListener(timelineEventsChangeListener)
+        chunkEntity.addChangeListener(chunkObjectListener)
+    }
+
+    fun hasReachedLastForward(): Boolean {
+        return if (isLastForward.get()) {
+            true
+        } else {
+            nextChunk?.hasReachedLastForward().orFalse()
+        }
+    }
+
+    fun builtItems(includesNext: Boolean, includesPrev: Boolean): List<TimelineEvent> {
+        val deepBuiltItems = ArrayList<TimelineEvent>(builtEvents.size)
+        if (includesNext) {
+            val nextEvents = nextChunk?.builtItems(includesNext = true, includesPrev = false).orEmpty()
+            deepBuiltItems.addAll(nextEvents)
+        }
+        deepBuiltItems.addAll(builtEvents)
+        if (includesPrev) {
+            val prevEvents = prevChunk?.builtItems(includesNext = false, includesPrev = true).orEmpty()
+            deepBuiltItems.addAll(prevEvents)
+        }
+        return deepBuiltItems
+    }
+
+    /**
+     * This will take care of loading and building events of this chunk for the given direction and count.
+     * If @param fetchFromServerIfNeeded is true, it will try to fetch more events on server to get the right amount of data.
+     * This method will also post a snapshot as soon the data is built from db to avoid waiting for server response.
+     */
+    suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
+        if (direction == Timeline.Direction.FORWARDS && nextChunk != null) {
+            return nextChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
+        } else if (direction == Timeline.Direction.BACKWARDS && prevChunk != null) {
+            return prevChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
+        }
+        val loadFromStorageCount = loadFromStorage(count, direction)
+        Timber.v("Has loaded $loadFromStorageCount items from storage in $direction")
+        val offsetCount = count - loadFromStorageCount
+        return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) {
+            LoadMoreResult.REACHED_END
+        } else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) {
+            LoadMoreResult.REACHED_END
+        } else if (offsetCount == 0) {
+            LoadMoreResult.SUCCESS
+        } else {
+            delegateLoadMore(fetchOnServerIfNeeded, offsetCount, direction)
+        }
+    }
+
+    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?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE
+                }
+                fetchFromServerIfNeeded -> {
+                    fetchFromServer(offsetCount, chunkEntity.nextToken, direction)
+                }
+                else                    -> {
+                    LoadMoreResult.SUCCESS
+                }
+            }
+        } else {
+            val prevChunkEntity = chunkEntity.prevChunk
+            when {
+                prevChunkEntity != null -> {
+                    if (prevChunk == null) {
+                        prevChunk = createTimelineChunk(prevChunkEntity)
+                    }
+                    prevChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE
+                }
+                fetchFromServerIfNeeded -> {
+                    fetchFromServer(offsetCount, chunkEntity.prevToken, direction)
+                }
+                else                    -> {
+                    LoadMoreResult.SUCCESS
+                }
+            }
+        }
+    }
+
+    fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? {
+        val builtEventIndex = builtEventsIndexes[eventId]
+        if (builtEventIndex != null) {
+            return getOffsetIndex() + builtEventIndex
+        }
+        if (searchInNext) {
+            val nextBuiltEventIndex = nextChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = false)
+            if (nextBuiltEventIndex != null) {
+                return nextBuiltEventIndex
+            }
+        }
+        if (searchInPrev) {
+            val prevBuiltEventIndex = prevChunk?.getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = true)
+            if (prevBuiltEventIndex != null) {
+                return prevBuiltEventIndex
+            }
+        }
+        return null
+    }
+
+    fun getBuiltEvent(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): TimelineEvent? {
+        val builtEventIndex = builtEventsIndexes[eventId]
+        if (builtEventIndex != null) {
+            return builtEvents.getOrNull(builtEventIndex)
+        }
+        if (searchInNext) {
+            val nextBuiltEvent = nextChunk?.getBuiltEvent(eventId, searchInNext = true, searchInPrev = false)
+            if (nextBuiltEvent != null) {
+                return nextBuiltEvent
+            }
+        }
+        if (searchInPrev) {
+            val prevBuiltEvent = prevChunk?.getBuiltEvent(eventId, searchInNext = false, searchInPrev = true)
+            if (prevBuiltEvent != null) {
+                return prevBuiltEvent
+            }
+        }
+        return null
+    }
+
+    fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?, searchInNext: Boolean, searchInPrev: Boolean): Boolean {
+        return tryOrNull {
+            val builtIndex = getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = false)
+            if (builtIndex == null) {
+                val foundInPrev = searchInPrev && prevChunk?.rebuildEvent(eventId, builder, searchInNext = false, searchInPrev = true).orFalse()
+                if (foundInPrev) {
+                    return true
+                }
+                if (searchInNext) {
+                    return prevChunk?.rebuildEvent(eventId, builder, searchInPrev = false, searchInNext = true).orFalse()
+                }
+                return false
+            }
+            // Update the relation of existing event
+            builtEvents.getOrNull(builtIndex)?.let { te ->
+                val rebuiltEvent = builder(te)
+                builtEvents[builtIndex] = rebuiltEvent!!
+                true
+            }
+        }
+                ?: false
+    }
+
+    fun close(closeNext: Boolean, closePrev: Boolean) {
+        if (closeNext) {
+            nextChunk?.close(closeNext = true, closePrev = false)
+        }
+        if (closePrev) {
+            prevChunk?.close(closeNext = false, closePrev = true)
+        }
+        nextChunk = null
+        nextChunkLatch?.cancel()
+        prevChunk = null
+        prevChunkLatch?.cancel()
+        chunkEntity.removeChangeListener(chunkObjectListener)
+        timelineEventEntities.removeChangeListener(timelineEventsChangeListener)
+    }
+
+    /**
+     * This method tries to read events from the current chunk.
+     */
+    private suspend fun loadFromStorage(count: Int, direction: Timeline.Direction): Int {
+        val displayIndex = getNextDisplayIndex(direction) ?: return 0
+        val baseQuery = timelineEventEntities.where()
+        val timelineEvents = baseQuery.offsets(direction, count, displayIndex).findAll().orEmpty()
+        if (timelineEvents.isEmpty()) return 0
+        fetchRootThreadEventsIfNeeded(timelineEvents)
+        if (direction == Timeline.Direction.FORWARDS) {
+            builtEventsIndexes.entries.forEach { it.setValue(it.value + timelineEvents.size) }
+        }
+        timelineEvents
+                .mapIndexed { index, timelineEventEntity ->
+                    val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded()
+                    if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
+                        isLastBackward.set(true)
+                    }
+                    if (direction == Timeline.Direction.FORWARDS) {
+                        builtEventsIndexes[timelineEvent.eventId] = index
+                        builtEvents.add(index, timelineEvent)
+                    } else {
+                        builtEventsIndexes[timelineEvent.eventId] = builtEvents.size
+                        builtEvents.add(timelineEvent)
+                    }
+                }
+        return timelineEvents.size
+    }
+
+    /**
+     * This function is responsible to fetch and store the root event of a thread event
+     * in order to be able to display the event to the user appropriately
+     */
+    private suspend fun fetchRootThreadEventsIfNeeded(offsetResults: List<TimelineEventEntity>) {
+        val eventEntityList = offsetResults
+                .mapNotNull {
+                    it.root
+                }.map {
+                    EventMapper.map(it)
+                }
+        threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
+    }
+
+    private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent {
+        val timelineEvent = buildTimelineEvent(this)
+        val transactionId = timelineEvent.root.unsignedData?.transactionId
+        uiEchoManager?.onSyncedEvent(transactionId)
+        if (timelineEvent.isEncrypted() &&
+                timelineEvent.root.mxDecryptionResult == null) {
+            timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) }
+        }
+        return timelineEvent
+    }
+
+    private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map(
+            timelineEventEntity = eventEntity,
+            buildReadReceipts = timelineSettings.buildReadReceipts
+    ).let {
+        // eventually enhance with ui echo?
+        (uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it)
+    }
+
+    /**
+     * Will try to fetch a new chunk on the home server.
+     * It will take care to update the database by inserting new events and linking new chunk
+     * with this one.
+     */
+    private suspend fun fetchFromServer(count: Int, token: String?, direction: Timeline.Direction): LoadMoreResult {
+        val latch = if (direction == Timeline.Direction.FORWARDS) {
+            nextChunkLatch = CompletableDeferred()
+            nextChunkLatch
+        } else {
+            prevChunkLatch = CompletableDeferred()
+            prevChunkLatch
+        }
+        val loadMoreResult = try {
+            if (token == null) {
+                if (direction == Timeline.Direction.BACKWARDS || !chunkEntity.hasBeenALastForwardChunk()) return LoadMoreResult.REACHED_END
+                val lastKnownEventId = chunkEntity.sortedTimelineEvents().firstOrNull()?.eventId ?: return LoadMoreResult.FAILURE
+                val taskParams = FetchTokenAndPaginateTask.Params(roomId, lastKnownEventId, direction.toPaginationDirection(), count)
+                fetchTokenAndPaginateTask.execute(taskParams).toLoadMoreResult()
+            } else {
+                Timber.v("Fetch $count more events on server")
+                val taskParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), count)
+                paginationTask.execute(taskParams).toLoadMoreResult()
+            }
+        } catch (failure: Throwable) {
+            Timber.e("Failed to fetch from server: $failure", failure)
+            LoadMoreResult.FAILURE
+        }
+        return if (loadMoreResult == LoadMoreResult.SUCCESS) {
+            latch?.await()
+            loadMore(count, direction, fetchOnServerIfNeeded = false)
+        } else {
+            loadMoreResult
+        }
+    }
+
+    private fun TokenChunkEventPersistor.Result.toLoadMoreResult(): LoadMoreResult {
+        return when (this) {
+            TokenChunkEventPersistor.Result.REACHED_END -> LoadMoreResult.REACHED_END
+            TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE,
+            TokenChunkEventPersistor.Result.SUCCESS     -> LoadMoreResult.SUCCESS
+        }
+    }
+
+    private fun getOffsetIndex(): Int {
+        var offset = 0
+        var currentNextChunk = nextChunk
+        while (currentNextChunk != null) {
+            offset += currentNextChunk.builtEvents.size
+            currentNextChunk = currentNextChunk.nextChunk
+        }
+        return offset
+    }
+
+    /**
+     * This method is responsible for managing insertions and updates of events on this chunk.
+     *
+     */
+    private fun handleDatabaseChangeSet(frozenResults: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
+        val insertions = changeSet.insertionRanges
+        for (range in insertions) {
+            val newItems = frozenResults
+                    .subList(range.startIndex, range.startIndex + range.length)
+                    .map { it.buildAndDecryptIfNeeded() }
+            builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) }
+            newItems.mapIndexed { index, timelineEvent ->
+                if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
+                    isLastBackward.set(true)
+                }
+                val correctedIndex = range.startIndex + index
+                builtEvents.add(correctedIndex, timelineEvent)
+                builtEventsIndexes[timelineEvent.eventId] = correctedIndex
+            }
+        }
+        val modifications = changeSet.changeRanges
+        for (range in modifications) {
+            for (modificationIndex in (range.startIndex until range.startIndex + range.length)) {
+                val updatedEntity = frozenResults[modificationIndex] ?: continue
+                try {
+                    builtEvents[modificationIndex] = updatedEntity.buildAndDecryptIfNeeded()
+                } catch (failure: Throwable) {
+                    Timber.v("Fail to update items at index: $modificationIndex")
+                }
+            }
+        }
+        if (insertions.isNotEmpty() || modifications.isNotEmpty()) {
+            onBuiltEvents(true)
+        }
+    }
+
+    private fun getNextDisplayIndex(direction: Timeline.Direction): Int? {
+        val frozenTimelineEvents = timelineEventEntities.freeze()
+        if (frozenTimelineEvents.isEmpty()) {
+            return null
+        }
+        return if (builtEvents.isEmpty()) {
+            if (initialEventId != null) {
+                frozenTimelineEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex
+            } else if (direction == Timeline.Direction.BACKWARDS) {
+                frozenTimelineEvents.first()?.displayIndex
+            } else {
+                frozenTimelineEvents.last()?.displayIndex
+            }
+        } else if (direction == Timeline.Direction.FORWARDS) {
+            builtEvents.first().displayIndex + 1
+        } else {
+            builtEvents.last().displayIndex - 1
+        }
+    }
+
+    private fun createTimelineChunk(chunkEntity: ChunkEntity?): TimelineChunk? {
+        if (chunkEntity == null) return null
+        return TimelineChunk(
+                chunkEntity = chunkEntity,
+                timelineSettings = timelineSettings,
+                roomId = roomId,
+                timelineId = timelineId,
+                eventDecryptor = eventDecryptor,
+                paginationTask = paginationTask,
+                fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
+                timelineEventMapper = timelineEventMapper,
+                uiEchoManager = uiEchoManager,
+                threadsAwarenessHandler = threadsAwarenessHandler,
+                initialEventId = null,
+                onBuiltEvents = this.onBuiltEvents
+        )
+    }
+}
+
+private fun RealmQuery<TimelineEventEntity>.offsets(
+        direction: Timeline.Direction,
+        count: Int,
+        startDisplayIndex: Int
+): RealmQuery<TimelineEventEntity> {
+    sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
+    if (direction == Timeline.Direction.BACKWARDS) {
+        lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
+    } else {
+        greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
+    }
+    return limit(count.toLong())
+}
+
+private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
+    return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
+}
+
+private fun ChunkEntity.sortedTimelineEvents(): RealmResults<TimelineEventEntity> {
+    return timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt
index cdc85ea722793334bfb8c1d2b7db38967ee58ae4..a953db07044ac19c36707ac38eb1b6a367c4db58 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt
@@ -23,6 +23,9 @@ import javax.inject.Inject
 
 @SessionScope
 internal class TimelineInput @Inject constructor() {
+
+    val listeners = mutableSetOf<Listener>()
+
     fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) {
         listeners.toSet().forEach { it.onLocalEchoCreated(roomId, timelineEvent) }
     }
@@ -35,8 +38,6 @@ internal class TimelineInput @Inject constructor() {
         listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) }
     }
 
-    val listeners = mutableSetOf<Listener>()
-
     internal interface Listener {
         fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit
         fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit
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 dbcc37a918a6d39a11836cebb0d31280d32d70c1..a85f0dbdc93a35f6626feaa6bf8f415d003755e7 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
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.session.room.timeline
 
 import com.zhuinden.monarchy.Monarchy
+import dagger.Lazy
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
@@ -25,93 +26,27 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.internal.database.helper.addIfNecessary
 import org.matrix.android.sdk.internal.database.helper.addStateEvent
 import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
-import org.matrix.android.sdk.internal.database.helper.merge
 import org.matrix.android.sdk.internal.database.mapper.toEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.model.RoomEntity
-import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
-import org.matrix.android.sdk.internal.database.model.deleteOnCascade
+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.create
 import org.matrix.android.sdk.internal.database.query.find
-import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
-import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
-import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper
+import org.matrix.android.sdk.internal.session.StreamEventsManager
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import timber.log.Timber
 import javax.inject.Inject
 
 /**
- * Insert Chunk in DB, and eventually merge with existing chunk event
+ * Insert Chunk in DB, and eventually link next and previous chunk in db.
  */
-internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase private val monarchy: Monarchy) {
-
-    /**
-     * <pre>
-     * ========================================================================================================
-     * | Backward case                                                                                        |
-     * ========================================================================================================
-     *
-     *                               *--------------------------*        *--------------------------*
-     *                               | startToken1              |        | startToken1              |
-     *                               *--------------------------*        *--------------------------*
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |  receivedChunk backward  |        |                          |
-     *                               |         Events           |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     * *--------------------------*  *--------------------------*        |                          |
-     * | startToken0              |  | endToken1                |   =>   |       Merged chunk       |
-     * *--------------------------*  *--------------------------*        |          Events          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |      Current Chunk       |                                      |                          |
-     * |         Events           |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * *--------------------------*                                      *--------------------------*
-     * | endToken0                |                                      | endToken0                |
-     * *--------------------------*                                      *--------------------------*
-     *
-     *
-     * ========================================================================================================
-     * | Forward case                                                                                         |
-     * ========================================================================================================
-     *
-     * *--------------------------*                                      *--------------------------*
-     * | startToken0              |                                      | startToken0              |
-     * *--------------------------*                                      *--------------------------*
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |      Current Chunk       |                                      |                          |
-     * |         Events           |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * *--------------------------*  *--------------------------*        |                          |
-     * | endToken0                |  | startToken1              |   =>   |       Merged chunk       |
-     * *--------------------------*  *--------------------------*        |          Events          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |  receivedChunk forward   |        |                          |
-     *                               |         Events           |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               *--------------------------*        *--------------------------*
-     *                               | endToken1                |        | endToken1                |
-     *                               *--------------------------*        *--------------------------*
-     *
-     * ========================================================================================================
-     * </pre>
-     */
+internal class TokenChunkEventPersistor @Inject constructor(
+        @SessionDatabase private val monarchy: Monarchy,
+        private val liveEventManager: Lazy<StreamEventsManager>) {
 
     enum class Result {
         SHOULD_FETCH_MORE,
@@ -136,21 +71,21 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
                         prevToken = receivedChunk.end
                     }
 
+                    val existingChunk = ChunkEntity.find(realm, roomId, prevToken = prevToken, nextToken = nextToken)
+                    if (existingChunk != null) {
+                        Timber.v("This chunk is already in the db, returns")
+                        return@awaitTransaction
+                    }
                     val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
                     val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
-
-                    // The current chunk is the one we will keep all along the merge processChanges.
-                    // We try to look for a chunk next to the token,
-                    // otherwise we create a whole new one which is unlinked (not live)
-                    val currentChunk = if (direction == PaginationDirection.FORWARDS) {
-                        prevChunk?.apply { this.nextToken = nextToken }
-                    } else {
-                        nextChunk?.apply { this.prevToken = prevToken }
+                    val currentChunk = ChunkEntity.create(realm, prevToken = prevToken, nextToken = nextToken).apply {
+                        this.nextChunk = nextChunk
+                        this.prevChunk = prevChunk
                     }
-                            ?: ChunkEntity.create(realm, prevToken, nextToken)
-
-                    if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) {
-                        handleReachEnd(realm, roomId, direction, currentChunk)
+                    nextChunk?.prevChunk = currentChunk
+                    prevChunk?.nextChunk = currentChunk
+                    if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
+                        handleReachEnd(roomId, direction, currentChunk)
                     } else {
                         handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
                     }
@@ -166,17 +101,10 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
         }
     }
 
-    private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
+    private fun handleReachEnd(roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
         Timber.v("Reach end of $roomId")
         if (direction == PaginationDirection.FORWARDS) {
-            val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
-            if (currentChunk != currentLastForwardChunk) {
-                currentChunk.isLastForward = true
-                currentLastForwardChunk?.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = false)
-                RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
-                    latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
-                }
-            }
+            Timber.v("We should keep the lastForward chunk unique, the one from sync")
         } else {
             currentChunk.isLastBackward = true
         }
@@ -204,45 +132,52 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
                 roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel<RoomMemberContent>()
             }
         }
-        val eventIds = ArrayList<String>(eventList.size)
-        eventList.forEach { event ->
-            if (event.eventId == null || event.senderId == null) {
-                return@forEach
-            }
-            val ageLocalTs = event.unsignedData?.age?.let { now - it }
-            eventIds.add(event.eventId)
-            val 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
-                } else {
-                    event.content
+        run processTimelineEvents@{
+            eventList.forEach { event ->
+                if (event.eventId == null || event.senderId == null) {
+                    return@forEach
                 }
-                roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
-            }
-
-            currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
-        }
-        // Find all the chunks which contain at least one event from the list of eventIds
-        val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
-        Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds")
-        val chunksToDelete = ArrayList<ChunkEntity>()
-        chunks.forEach {
-            if (it != currentChunk) {
-                Timber.d("Merge $it")
-                currentChunk.merge(roomId, it, direction)
-                chunksToDelete.add(it)
+                // We check for the timeline event with this id
+                val eventId = event.eventId
+                val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
+                // If it exists, we want to stop here, just link the prevChunk
+                val existingChunk = existingTimelineEvent?.chunk?.firstOrNull()
+                if (existingChunk != null) {
+                    when (direction) {
+                        PaginationDirection.BACKWARDS -> {
+                            if (currentChunk.nextChunk == existingChunk) {
+                                Timber.w("Avoid double link, shouldn't happen in an ideal world")
+                            } else {
+                                currentChunk.prevChunk = existingChunk
+                                existingChunk.nextChunk = currentChunk
+                            }
+                        }
+                        PaginationDirection.FORWARDS  -> {
+                            if (currentChunk.prevChunk == existingChunk) {
+                                Timber.w("Avoid double link, shouldn't happen in an ideal world")
+                            } else {
+                                currentChunk.nextChunk = existingChunk
+                                existingChunk.prevChunk = currentChunk
+                            }
+                        }
+                    }
+                    // Stop processing here
+                    return@processTimelineEvents
+                }
+                val ageLocalTs = event.unsignedData?.age?.let { now - it }
+                val 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
+                    } else {
+                        event.content
+                    }
+                    roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
+                }
+                liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
+                currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
             }
         }
-        chunksToDelete.forEach {
-            it.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = false)
-        }
-        val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
-        val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null ||
-                (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
-        if (shouldUpdateSummary) {
-            roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
-        }
         if (currentChunk.isValid) {
             RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt
index 4804fbd73143d2ec0b5db1e271d94c717e542bb6..16d36c0cd92e1a5af8776f5897628628e461ffec 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt
@@ -24,14 +24,10 @@ import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
 import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
-import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
 import timber.log.Timber
 import java.util.Collections
 
-internal class UIEchoManager(
-        private val settings: TimelineSettings,
-        private val listener: Listener
-) {
+internal class UIEchoManager(private val listener: Listener) {
 
     interface Listener {
         fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean
@@ -70,13 +66,12 @@ internal class UIEchoManager(
         return existingState != sendState
     }
 
-    fun onLocalEchoCreated(timelineEvent: TimelineEvent)  {
-        // Manage some ui echos (do it before filter because actual event could be filtered out)
+    fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean  {
         when (timelineEvent.root.getClearType()) {
             EventType.REDACTION -> {
             }
             EventType.REACTION -> {
-                val content = timelineEvent.root.content?.toModel<ReactionContent>()
+                val content: ReactionContent? = timelineEvent.root.content?.toModel<ReactionContent>()
                 if (RelationType.ANNOTATION == content?.relatesTo?.type) {
                     val reaction = content.relatesTo.key
                     val relatedEventID = content.relatesTo.eventId
@@ -96,11 +91,12 @@ internal class UIEchoManager(
         }
         Timber.v("On local echo created: ${timelineEvent.eventId}")
         inMemorySendingEvents.add(0, timelineEvent)
+        return true
     }
 
-    fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? {
+    fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent {
         val relatedEventID = timelineEvent.eventId
-        val contents = inMemoryReactions[relatedEventID] ?: return null
+        val contents = inMemoryReactions[relatedEventID] ?: return timelineEvent
 
         var existingAnnotationSummary = timelineEvent.annotations ?: EventAnnotationsSummary(
                 relatedEventID
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 1a7e15e14c489f4e63f9abca9d0e0f5e6930f341..24722445be4842e186218cdd70ab3fd29ba0254b 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
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.sync.handler.room
 
+import dagger.Lazy
 import io.realm.Realm
 import io.realm.kotlin.createObject
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@@ -52,6 +53,7 @@ import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.extensions.clearWith
+import org.matrix.android.sdk.internal.session.StreamEventsManager
 import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
 import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
 import org.matrix.android.sdk.internal.session.initsync.mapWithProgress
@@ -79,7 +81,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val threadsAwarenessHandler: ThreadsAwarenessHandler,
                                                    private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
                                                    @UserId private val userId: String,
-                                                   private val timelineInput: TimelineInput) {
+                                                   private val timelineInput: TimelineInput,
+                                                   private val liveEventService: Lazy<StreamEventsManager>) {
 
     sealed class HandlingStrategy {
         data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
@@ -218,6 +221,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 }
                 val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
                 val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
+                Timber.v("## received state event ${event.type} and key ${event.stateKey}")
                 CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
                     // Timber.v("## Space state event: $eventEntity")
                     eventId = event.eventId
@@ -345,15 +349,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                      syncLocalTimestampMillis: Long,
                                      aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
         val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
+        if (isLimited && lastChunk != null) {
+            lastChunk.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true)
+        }
         val chunkEntity = if (!isLimited && lastChunk != null) {
             lastChunk
         } else {
-            realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
+            realm.createObject<ChunkEntity>().apply {
+                this.prevToken = prevToken
+                this.isLastForward = true
+            }
         }
-        // Only one chunk has isLastForward set to true
-        lastChunk?.isLastForward = false
-        chunkEntity.isLastForward = true
-
         val eventIds = ArrayList<String>(eventList.size)
         val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
 
@@ -362,6 +368,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 continue
             }
             eventIds.add(event.eventId)
+            liveEventService.get().dispatchLiveEventReceived(event, roomId, insertType == EventInsertType.INITIAL_SYNC)
 
             val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
 
@@ -387,6 +394,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                     roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent, aggregator)
                 }
             }
+
             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
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
index 3faa0c9488a44d51ad861d984c81c5a6ff019b00..b6ea7a68f76fdbfc5bc2ce7e3fec2176e2d45946 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
@@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.isTokenError
 import org.matrix.android.sdk.api.logger.LoggerTag
@@ -71,6 +72,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
     private var isStarted = false
     private var isTokenValid = true
     private var retryNoNetworkTask: TimerTask? = null
+    private var previousSyncResponseHasToDevice = false
 
     private val activeCallListObserver = Observer<MutableList<MxCall>> { activeCalls ->
         if (activeCalls.isEmpty() && backgroundDetectionObserver.isInBackground) {
@@ -171,12 +173,15 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
                 if (state !is SyncState.Running) {
                     updateStateTo(SyncState.Running(afterPause = true))
                 }
-                // No timeout after a pause
-                val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
+                val timeout = when {
+                    previousSyncResponseHasToDevice                        -> 0L /* Force timeout to 0 */
+                    state.let { it is SyncState.Running && it.afterPause } -> 0L /* No timeout after a pause */
+                    else                                                   -> DEFAULT_LONG_POOL_TIMEOUT
+                }
                 Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout")
                 val params = SyncTask.Params(timeout, SyncPresence.Online)
                 val sync = syncScope.launch {
-                    doSync(params)
+                    previousSyncResponseHasToDevice = doSync(params)
                 }
                 runBlocking {
                     sync.join()
@@ -203,10 +208,14 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
         }
     }
 
-    private suspend fun doSync(params: SyncTask.Params) {
-        try {
+    /**
+     * Will return true if the sync response contains some toDevice events.
+     */
+    private suspend fun doSync(params: SyncTask.Params): Boolean {
+        return try {
             val syncResponse = syncTask.execute(params)
             _syncFlow.emit(syncResponse)
+            syncResponse.toDevice?.events?.isNotEmpty().orFalse()
         } catch (failure: Throwable) {
             if (failure is Failure.NetworkConnection) {
                 canReachServer = false
@@ -229,6 +238,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
                     delay(RETRY_WAIT_TIME_MS)
                 }
             }
+            false
         } finally {
             state.let {
                 if (it is SyncState.Running && it.afterPause) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt
index 763cd55714b4885ded32315a3c2fc19f6799e86b..2f1241f4d8f15680ab69ba5d73d61400d9aa22e8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt
@@ -20,6 +20,7 @@ import androidx.work.BackoffPolicy
 import androidx.work.ExistingWorkPolicy
 import androidx.work.WorkerParameters
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.failure.isTokenError
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.di.WorkManagerProvider
@@ -34,8 +35,8 @@ import timber.log.Timber
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
-private const val DEFAULT_LONG_POOL_TIMEOUT = 6L
-private const val DEFAULT_DELAY_TIMEOUT = 30_000L
+private const val DEFAULT_LONG_POOL_TIMEOUT_SECONDS = 6L
+private const val DEFAULT_DELAY_MILLIS = 30_000L
 
 /**
  * Possible previous worker: None
@@ -47,9 +48,12 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
     @JsonClass(generateAdapter = true)
     internal data class Params(
             override val sessionId: String,
-            val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT,
-            val delay: Long = DEFAULT_DELAY_TIMEOUT,
+            // In seconds
+            val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT_SECONDS,
+            // In milliseconds
+            val delay: Long = DEFAULT_DELAY_MILLIS,
             val periodic: Boolean = false,
+            val forceImmediate: Boolean = false,
             override val lastFailureMessage: String? = null
     ) : SessionWorkerParams
 
@@ -65,13 +69,26 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
         Timber.i("Sync work starting")
 
         return runCatching {
-            doSync(params.timeout)
+            doSync(if (params.forceImmediate) 0 else params.timeout)
         }.fold(
-                {
+                { hasToDeviceEvents ->
                     Result.success().also {
                         if (params.periodic) {
-                            // we want to schedule another one after delay
-                            automaticallyBackgroundSync(workManagerProvider, params.sessionId, params.timeout, params.delay)
+                            // we want to schedule another one after a delay, or immediately if hasToDeviceEvents
+                            automaticallyBackgroundSync(
+                                    workManagerProvider = workManagerProvider,
+                                    sessionId = params.sessionId,
+                                    serverTimeoutInSeconds = params.timeout,
+                                    delayInSeconds = params.delay,
+                                    forceImmediate = hasToDeviceEvents
+                            )
+                        } else if (hasToDeviceEvents) {
+                            // Previous response has toDevice events, request an immediate sync request
+                            requireBackgroundSync(
+                                    workManagerProvider = workManagerProvider,
+                                    sessionId = params.sessionId,
+                                    serverTimeoutInSeconds = 0
+                            )
                         }
                     }
                 },
@@ -92,16 +109,29 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
         return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
     }
 
-    private suspend fun doSync(timeout: Long) {
+    /**
+     * Will return true if the sync response contains some toDevice events.
+     */
+    private suspend fun doSync(timeout: Long): Boolean {
         val taskParams = SyncTask.Params(timeout * 1000, SyncPresence.Offline)
-        syncTask.execute(taskParams)
+        val syncResponse = syncTask.execute(taskParams)
+        return syncResponse.toDevice?.events?.isNotEmpty().orFalse()
     }
 
     companion object {
         private const val BG_SYNC_WORK_NAME = "BG_SYNCP"
 
-        fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) {
-            val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, 0L, false))
+        fun requireBackgroundSync(workManagerProvider: WorkManagerProvider,
+                                  sessionId: String,
+                                  serverTimeoutInSeconds: Long = 0) {
+            val data = WorkerParamsFactory.toData(
+                    Params(
+                            sessionId = sessionId,
+                            timeout = serverTimeoutInSeconds,
+                            delay = 0L,
+                            periodic = false
+                    )
+            )
             val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
                     .setConstraints(WorkManagerProvider.workConstraints)
                     .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
@@ -111,13 +141,24 @@ internal class SyncWorker(context: Context, workerParameters: WorkerParameters,
                     .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
         }
 
-        fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delayInSeconds: Long = 30) {
-            val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, delayInSeconds, true))
+        fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider,
+                                        sessionId: String,
+                                        serverTimeoutInSeconds: Long = 0,
+                                        delayInSeconds: Long = 30,
+                                        forceImmediate: Boolean = false) {
+            val data = WorkerParamsFactory.toData(
+                    Params(
+                            sessionId = sessionId,
+                            timeout = serverTimeoutInSeconds,
+                            delay = delayInSeconds,
+                            forceImmediate = forceImmediate
+                    )
+            )
             val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
                     .setConstraints(WorkManagerProvider.workConstraints)
                     .setInputData(data)
                     .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
-                    .setInitialDelay(delayInSeconds, TimeUnit.SECONDS)
+                    .setInitialDelay(if (forceImmediate) 0 else delayInSeconds, TimeUnit.SECONDS)
                     .build()
             // Avoid risking multiple chains of syncs by replacing the existing chain
             workManagerProvider.workManager
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt
index c52c6a404edbd8c341c745eca9149dfb80b5bf04..313fb6319d5ef3c32a4cbbb06d68d1997b12b7e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt
@@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.sync.model.accountdata.AcceptedTe
 import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask
 import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource
 import org.matrix.android.sdk.internal.util.ensureTrailingSlash
+import timber.log.Timber
 import javax.inject.Inject
 
 internal class DefaultTermsService @Inject constructor(
@@ -63,19 +64,28 @@ internal class DefaultTermsService @Inject constructor(
      */
     override suspend fun getHomeserverTerms(baseUrl: String): TermsResponse {
         return try {
+            val request = baseUrl + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register"
             executeRequest(null) {
-                termsAPI.register(baseUrl + NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
+                termsAPI.register(request)
             }
             // Return empty result if it succeed, but it should never happen
+            Timber.w("Request $request succeeded, it should never happen")
             TermsResponse()
         } catch (throwable: Throwable) {
-            @Suppress("UNCHECKED_CAST")
-            TermsResponse(
-                    policies = (throwable.toRegistrationFlowResponse()
-                            ?.params
-                            ?.get(LoginFlowTypes.TERMS) as? JsonDict)
-                            ?.get("policies") as? JsonDict
-            )
+            val registrationFlowResponse = throwable.toRegistrationFlowResponse()
+            if (registrationFlowResponse != null) {
+                @Suppress("UNCHECKED_CAST")
+                TermsResponse(
+                        policies = (registrationFlowResponse
+                                .params
+                                ?.get(LoginFlowTypes.TERMS) as? JsonDict)
+                                ?.get("policies") as? JsonDict
+                )
+            } else {
+                // Other error
+                Timber.e(throwable, "Error while getting homeserver terms")
+                throw throwable
+            }
         }
     }