diff --git a/dependencies.gradle b/dependencies.gradle
index dc66de43eac191375fff013b9fce6ea655b9249e..31c32bb26b5cae8b9eacb07e01fa42c556d5f3a3 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -10,7 +10,7 @@ def gradle = "7.3.1"
 // Ref: https://kotlinlang.org/releases.html
 def kotlin = "1.7.21"
 def kotlinCoroutines = "1.6.4"
-def dagger = "2.44"
+def dagger = "2.44.2"
 def appDistribution = "16.0.0-beta05"
 def retrofit = "2.9.0"
 def markwon = "4.6.2"
@@ -83,7 +83,7 @@ ext.libs = [
                 'appdistributionApi'      : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
                 'appdistribution'         : "com.google.firebase:firebase-appdistribution:$appDistribution",
                 // Phone number https://github.com/google/libphonenumber
-                'phonenumber'             : "com.googlecode.libphonenumber:libphonenumber:8.13.0"
+                'phonenumber'             : "com.googlecode.libphonenumber:libphonenumber:8.13.1"
         ],
         dagger      : [
                 'dagger'                  : "com.google.dagger:dagger:$dagger",
@@ -98,7 +98,7 @@ ext.libs = [
         ],
         element     : [
                 'opusencoder'             : "io.element.android:opusencoder:1.1.0",
-                'wysiwyg'                 : "io.element.android:wysiwyg:0.4.0"
+                'wysiwyg'                 : "io.element.android:wysiwyg:0.7.0.1"
         ],
         squareup    : [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",
diff --git a/matrix-sdk-android/src/androidTest/assets/session_42.realm b/matrix-sdk-android/src/androidTest/assets/session_42.realm
new file mode 100644
index 0000000000000000000000000000000000000000..b92d13dab2fa6d44a38ee3443ae8cad85340db23
Binary files /dev/null and b/matrix-sdk-android/src/androidTest/assets/session_42.realm differ
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 eeb2def5827876275de455c482a8090c7b32d6d6..8edecb273d10930891eae78af0edc030e1343732 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
@@ -50,6 +50,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
 import timber.log.Timber
 import java.util.UUID
 import java.util.concurrent.CountDownLatch
@@ -346,6 +347,10 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
         assertTrue(registrationResult is RegistrationResult.Success)
         val session = (registrationResult as RegistrationResult.Success).session
         session.open()
+        session.filterService().setSyncFilter(
+                SyncFilterBuilder()
+                        .lazyLoadMembersForStateEvents(true)
+        )
         if (sessionTestParams.withInitialSync) {
             syncSession(session, 120_000)
         }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e74aa52495fd73c707f3772c475e46679b77c17b
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration43Test.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import io.realm.Realm
+import org.amshove.kluent.fail
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldNotBe
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+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.internal.database.mapper.EventMapper
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
+import org.matrix.android.sdk.internal.database.model.SessionRealmModule
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.util.Normalizer
+
+@RunWith(AndroidJUnit4::class)
+class RealmSessionStoreMigration43Test {
+
+    @get:Rule val configurationFactory = TestRealmConfigurationFactory()
+
+    lateinit var context: Context
+    var realm: Realm? = null
+
+    @Before
+    fun setUp() {
+        context = InstrumentationRegistry.getInstrumentation().context
+    }
+
+    @After
+    fun tearDown() {
+        realm?.close()
+    }
+
+    @Test
+    fun migrationShouldBeNeeed() {
+        val realmName = "session_42.realm"
+        val realmConfiguration = configurationFactory.createConfiguration(
+                realmName,
+                "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
+                SessionRealmModule(),
+                43,
+                null
+        )
+        configurationFactory.copyRealmFromAssets(context, realmName, realmName)
+
+        try {
+            realm = Realm.getInstance(realmConfiguration)
+            fail("Should need a migration")
+        } catch (failure: Throwable) {
+            // nop
+        }
+    }
+
+    //  Database key for alias `session_db_e00482619b2597069b1f192b86de7da9`: efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0
+    // $WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI
+    // $11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo
+    @Test
+    fun testMigration43() {
+        val realmName = "session_42.realm"
+        val migration = RealmSessionStoreMigration(Normalizer())
+        val realmConfiguration = configurationFactory.createConfiguration(
+                realmName,
+                "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
+                SessionRealmModule(),
+                43,
+                migration
+        )
+        configurationFactory.copyRealmFromAssets(context, realmName, realmName)
+
+        realm = Realm.getInstance(realmConfiguration)
+
+        // assert that the edit from 42 are migrated
+        val editions = EventAnnotationsSummaryEntity
+                .where(realm!!, "\$WEJ8U6Zsx3TDZx3qmHIOKh-mXe5kqL_MnPcIkStEwwI")
+                .findFirst()
+                ?.editSummary
+                ?.editions
+
+        editions shouldNotBe null
+        editions!!.size shouldBe 1
+        val firstEdition = editions.first()
+        firstEdition?.eventId shouldBeEqualTo "\$DvOyA8vJxwGfTaJG3OEJVcL4isShyaVDnprihy38W28"
+        firstEdition?.isLocalEcho shouldBeEqualTo false
+
+        val editEvent = EventMapper.map(firstEdition!!.event!!)
+        val body = editEvent.content.toModel<MessageContent>()?.body
+        body shouldBeEqualTo "* Message 2 with edit"
+
+        // assert that the edit from 42 are migrated
+        val editionsOfE2E = EventAnnotationsSummaryEntity
+                .where(realm!!, "\$11EtAQ8RYcudJVtw7e6B5Vm4ufCqKTOWKblY2U_wrpo")
+                .findFirst()
+                ?.editSummary
+                ?.editions
+
+        editionsOfE2E shouldNotBe null
+        editionsOfE2E!!.size shouldBe 1
+        val firstEditionE2E = editionsOfE2E.first()
+        firstEditionE2E?.eventId shouldBeEqualTo "\$HUwJOQRCJwfPv7XSKvBPcvncjM0oR3q2tGIIIdv9Zts"
+        firstEditionE2E?.isLocalEcho shouldBeEqualTo false
+
+        val editEventE2E = EventMapper.map(firstEditionE2E!!.event!!)
+        val body2 = editEventE2E.getClearContent().toModel<MessageContent>()?.body
+        body2 shouldBeEqualTo "* Message 2, e2e edit"
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fc1a78835be09000fe6ff221987e42b09e836996
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/SessionSanityMigrationTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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 android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import io.realm.Realm
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.matrix.android.sdk.internal.database.model.SessionRealmModule
+import org.matrix.android.sdk.internal.util.Normalizer
+
+@RunWith(AndroidJUnit4::class)
+class SessionSanityMigrationTest {
+
+    @get:Rule val configurationFactory = TestRealmConfigurationFactory()
+
+    lateinit var context: Context
+    var realm: Realm? = null
+
+    @Before
+    fun setUp() {
+        context = InstrumentationRegistry.getInstrumentation().context
+    }
+
+    @After
+    fun tearDown() {
+        realm?.close()
+    }
+
+    @Test
+    fun sessionDatabaseShouldMigrateGracefully() {
+        val realmName = "session_42.realm"
+        val migration = RealmSessionStoreMigration(Normalizer())
+        val realmConfiguration = configurationFactory.createConfiguration(
+                realmName,
+                "efa9ab2c77ae06b0e767ffdb1c45b12be3c77d48d94f1ac41a7cd1d637fc59ac41f869a250453074e21ce13cfe7ed535593e7d150c08ce2bad7a2ab8c7b841f0",
+                SessionRealmModule(),
+                migration.schemaVersion,
+                migration
+        )
+        configurationFactory.copyRealmFromAssets(context, realmName, realmName)
+
+        realm = Realm.getInstance(realmConfiguration)
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fc5a0172870cfccb509e6b0ce112e34fde34c423
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/database/TestRealmConfigurationFactory.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.matrix.android.sdk.internal.database
+
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import io.realm.RealmMigration
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.lang.IllegalStateException
+import java.util.Collections
+import java.util.Locale
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.Throws
+
+/**
+ * Based on https://github.com/realm/realm-java/blob/master/realm/realm-library/src/testUtils/java/io/realm/TestRealmConfigurationFactory.java
+ */
+class TestRealmConfigurationFactory : TemporaryFolder() {
+    private val map: Map<RealmConfiguration, Boolean> = ConcurrentHashMap()
+    private val configurations = Collections.newSetFromMap(map)
+    @get:Synchronized private var isUnitTestFailed = false
+    private var testName = ""
+    private var tempFolder: File? = null
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            @Throws(Throwable::class)
+            override fun evaluate() {
+                setTestName(description)
+                before()
+                try {
+                    base.evaluate()
+                } catch (throwable: Throwable) {
+                    setUnitTestFailed()
+                    throw throwable
+                } finally {
+                    after()
+                }
+            }
+        }
+    }
+
+    @Throws(Throwable::class)
+    override fun before() {
+        Realm.init(InstrumentationRegistry.getInstrumentation().targetContext)
+        super.before()
+    }
+
+    override fun after() {
+        try {
+            for (configuration in configurations) {
+                Realm.deleteRealm(configuration)
+            }
+        } catch (e: IllegalStateException) {
+            // Only throws the exception caused by deleting the opened Realm if the test case itself doesn't throw.
+            if (!isUnitTestFailed) {
+                throw e
+            }
+        } finally {
+            // This will delete the temp directory.
+            super.after()
+        }
+    }
+
+    @Throws(IOException::class)
+    override fun create() {
+        super.create()
+        tempFolder = File(super.getRoot(), testName)
+        check(!(tempFolder!!.exists() && !tempFolder!!.delete())) { "Could not delete folder: " + tempFolder!!.absolutePath }
+        check(tempFolder!!.mkdir()) { "Could not create folder: " + tempFolder!!.absolutePath }
+    }
+
+    override fun getRoot(): File {
+        checkNotNull(tempFolder) { "the temporary folder has not yet been created" }
+        return tempFolder!!
+    }
+
+    /**
+     * To be called in the [.apply].
+     */
+    protected fun setTestName(description: Description) {
+        testName = description.displayName
+    }
+
+    @Synchronized
+    fun setUnitTestFailed() {
+        isUnitTestFailed = true
+    }
+
+    // This builder creates a configuration that is *NOT* managed.
+    // You have to delete it yourself.
+    private fun createConfigurationBuilder(): RealmConfiguration.Builder {
+        return RealmConfiguration.Builder().directory(root)
+    }
+
+    fun String.decodeHex(): ByteArray {
+        check(length % 2 == 0) { "Must have an even length" }
+        return chunked(2)
+                .map { it.toInt(16).toByte() }
+                .toByteArray()
+    }
+
+    fun createConfiguration(
+            name: String,
+            key: String?,
+            module: Any,
+            schemaVersion: Long,
+            migration: RealmMigration?
+    ): RealmConfiguration {
+        val builder = createConfigurationBuilder()
+        builder
+                .directory(root)
+                .name(name)
+                .apply {
+                    if (key != null) {
+                        encryptionKey(key.decodeHex())
+                    }
+                }
+                .modules(module)
+                // Allow writes on UI
+                .allowWritesOnUiThread(true)
+                .schemaVersion(schemaVersion)
+                .apply {
+                    migration?.let { migration(it) }
+                }
+        val configuration = builder.build()
+        configurations.add(configuration)
+        return configuration
+    }
+
+    // Copies a Realm file from assets to temp dir
+    @Throws(IOException::class)
+    fun copyRealmFromAssets(context: Context, realmPath: String, newName: String) {
+        val config = RealmConfiguration.Builder()
+                .directory(root)
+                .name(newName)
+                .build()
+        copyRealmFromAssets(context, realmPath, config)
+    }
+
+    @Throws(IOException::class)
+    fun copyRealmFromAssets(context: Context, realmPath: String, config: RealmConfiguration) {
+        check(!File(config.path).exists()) { String.format(Locale.ENGLISH, "%s exists!", config.path) }
+        val outFile = File(config.realmDirectory, config.realmFileName)
+        copyFileFromAssets(context, realmPath, outFile)
+    }
+
+    @Throws(IOException::class)
+    fun copyFileFromAssets(context: Context, assetPath: String?, outFile: File?) {
+        var stream: InputStream? = null
+        var os: FileOutputStream? = null
+        try {
+            stream = context.assets.open(assetPath!!)
+            os = FileOutputStream(outFile)
+            val buf = ByteArray(1024)
+            var bytesRead: Int
+            while (stream.read(buf).also { bytesRead = it } > -1) {
+                os.write(buf, 0, bytesRead)
+            }
+        } finally {
+            if (stream != null) {
+                try {
+                    stream.close()
+                } catch (ignore: IOException) {
+                }
+            }
+            if (os != null) {
+                try {
+                    os.close()
+                } catch (ignore: IOException) {
+                }
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
index a37d2ce015f68a624c39d8cc944b49a8ec0fbba6..a52e3cd7c7cfe0fd9ed5636d1f738a225bd6095d 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
@@ -66,7 +66,7 @@ class PollAggregationTest : InstrumentedTest {
 
         val aliceEventsListener = object : Timeline.Listener {
             override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
-                snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START }?.let { pollEvent ->
+                snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START.values }?.let { pollEvent ->
                     val pollEventId = pollEvent.eventId
                     val pollContent = pollEvent.root.content?.toModel<MessagePollContent>()
                     val pollSummary = pollEvent.annotations?.pollResponseSummary
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt
index 239f7499934d29529addeada6abb10df241a93c4..5b2ab7746794925d1ec2d82df1a2b48b31421dd0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedAnnotation.kt
@@ -38,5 +38,4 @@ data class AggregatedAnnotation(
         override val limited: Boolean? = false,
         override val count: Int? = 0,
         val chunk: List<RelationChunkInfo>? = null
-
 ) : UnsignedRelationInfo
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
index ae8ed3941fa3d4396044186eab396204a293791d..6577a9b41e04d9bbdf2383604a679a8e3d0a42a3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
@@ -19,7 +19,8 @@ import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
 /**
- * <code>
+ * Server side relation aggregation.
+ * ```
  *  {
  *       "m.annotation": {
  *          "chunk": [
@@ -43,12 +44,13 @@ import com.squareup.moshi.JsonClass
  *           "count": 1
  *           }
  *      }
- * </code>
+ * ```
  */
 
 @JsonClass(generateAdapter = true)
 data class AggregatedRelations(
         @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
         @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
+        @Json(name = "m.replace") val replaces: AggregatedReplace? = null,
         @Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2ae091a1a447ffa964af4e5300268bf8bf0e4ab4
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedReplace.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.events.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * Note that there can be multiple events with an m.replace relationship to a given event (for example, if an event is edited multiple times).
+ * These should be aggregated by the homeserver.
+ * https://spec.matrix.org/v1.4/client-server-api/#server-side-aggregation-of-mreplace-relationships
+ *
+ */
+@JsonClass(generateAdapter = true)
+data class AggregatedReplace(
+        @Json(name = "event_id") val eventId: String? = null,
+        @Json(name = "origin_server_ts") val originServerTs: Long? = null,
+        @Json(name = "sender") val senderId: String? = null,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index 6ae585a27325a22529beca37add9090e14836e12..40ce6ecb5c11343d642bb3eb0462d3978475ff5e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -26,13 +26,12 @@ import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
 import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
+import org.matrix.android.sdk.api.session.room.model.relation.isReply
 import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.threads.ThreadDetails
@@ -228,11 +227,14 @@ data class Event(
         return when {
             isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
             isFileMessage() -> "sent a file."
+            isVoiceMessage() -> "sent a voice message."
             isAudioMessage() -> "sent an audio file."
             isImageMessage() -> "sent an image."
             isVideoMessage() -> "sent a video."
-            isSticker() -> "sent a sticker"
+            isSticker() -> "sent a sticker."
             isPoll() -> getPollQuestion() ?: "created a poll."
+            isLiveLocation() -> "Live location."
+            isLocationMessage() -> "has shared their location."
             else -> text
         }
     }
@@ -386,24 +388,18 @@ fun Event.isLocationMessage(): Boolean {
     }
 }
 
-fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END
+fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START.values || getClearType() in EventType.POLL_END.values
 
 fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
 
-fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO
+fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO.values
 
 fun Event.getRelationContent(): RelationDefaultContent? {
     return if (isEncrypted()) {
         content.toModel<EncryptedEventContent>()?.relatesTo
     } else {
-        content.toModel<MessageContent>()?.relatesTo ?: run {
-            // Special cases when there is only a local msgtype for some event types
-            when (getClearType()) {
-                EventType.STICKER -> getClearContent().toModel<MessageStickerContent>()?.relatesTo
-                in EventType.BEACON_LOCATION_DATA -> getClearContent().toModel<MessageBeaconLocationDataContent>()?.relatesTo
-                else -> getClearContent()?.get("m.relates_to")?.toContent().toModel()
-            }
-        }
+        content.toModel<MessageContent>()?.relatesTo
+                ?: getClearContent()?.get("m.relates_to")?.toContent().toModel() // Special cases when there is only a local msgtype for some event types
     }
 }
 
@@ -420,7 +416,7 @@ fun Event.getRelationContentForType(type: String): RelationDefaultContent? =
         getRelationContent()?.takeIf { it.type == type }
 
 fun Event.isReply(): Boolean {
-    return getRelationContent()?.inReplyTo?.eventId != null
+    return getRelationContent().isReply()
 }
 
 fun Event.isReplyRenderedInThread(): Boolean {
@@ -443,11 +439,11 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
         content?.toModel<RoomMemberContent>()?.membership == Membership.INVITE
 
 fun Event.getPollContent(): MessagePollContent? {
-    return content.toModel<MessagePollContent>()
+    return getClearContent().toModel<MessagePollContent>()
 }
 
 fun Event.supportsNotification() =
-        this.getClearType() in EventType.MESSAGE + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
+        this.getClearType() in EventType.MESSAGE + EventType.POLL_START.values + EventType.STATE_ROOM_BEACON_INFO.values
 
 fun Event.isContentReportable() =
-        this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO
+        this.getClearType() in EventType.MESSAGE + EventType.STATE_ROOM_BEACON_INFO.values
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt
new file mode 100644
index 0000000000000000000000000000000000000000..32d5ebed8ce1a2d7dd90206ceb33cbc4474b97ed
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventExt.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.events.model
+
+fun Event.toValidDecryptedEvent(): ValidDecryptedEvent? {
+    if (!this.isEncrypted()) return null
+    val decryptedContent = this.getDecryptedContent() ?: return null
+    val eventId = this.eventId ?: return null
+    val roomId = this.roomId ?: return null
+    val type = this.getDecryptedType() ?: return null
+    val senderKey = this.getSenderKey() ?: return null
+    val algorithm = this.content?.get("algorithm") as? String ?: return null
+
+    // copy the relation as it's in clear in the encrypted content
+    val updatedContent = this.content.get("m.relates_to")?.let {
+        decryptedContent.toMutableMap().apply {
+            put("m.relates_to", it)
+        }
+    } ?: decryptedContent
+    return ValidDecryptedEvent(
+            type = type,
+            eventId = eventId,
+            clearContent = updatedContent,
+            prevContent = this.prevContent,
+            originServerTs = this.originServerTs ?: 0,
+            cryptoSenderKey = senderKey,
+            roomId = roomId,
+            unsignedData = this.unsignedData,
+            redacts = this.redacts,
+            algorithm = algorithm
+    )
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
index 3ad4f3a87f08c7c03113541477989c7ec65670da..e5c14afa90550072d0d2ee437e9d0ac9c6dd164b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
@@ -49,11 +49,10 @@ object EventType {
     const val STATE_ROOM_JOIN_RULES = "m.room.join_rules"
     const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
     const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
-    val STATE_ROOM_BEACON_INFO = listOf("org.matrix.msc3672.beacon_info", "m.beacon_info")
-    val BEACON_LOCATION_DATA = listOf("org.matrix.msc3672.beacon", "m.beacon")
+    val STATE_ROOM_BEACON_INFO = StableUnstableId(stable = "m.beacon_info", unstable = "org.matrix.msc3672.beacon_info")
+    val BEACON_LOCATION_DATA = StableUnstableId(stable = "m.beacon", unstable = "org.matrix.msc3672.beacon")
 
     const val STATE_SPACE_CHILD = "m.space.child"
-
     const val STATE_SPACE_PARENT = "m.space.parent"
 
     /**
@@ -81,8 +80,7 @@ object EventType {
     const val CALL_NEGOTIATE = "m.call.negotiate"
     const val CALL_REJECT = "m.call.reject"
     const val CALL_HANGUP = "m.call.hangup"
-    const val CALL_ASSERTED_IDENTITY = "m.call.asserted_identity"
-    const val CALL_ASSERTED_IDENTITY_PREFIX = "org.matrix.call.asserted_identity"
+    val CALL_ASSERTED_IDENTITY = StableUnstableId(stable = "m.call.asserted_identity", unstable = "org.matrix.call.asserted_identity")
 
     // This type is not processed by the client, just sent to the server
     const val CALL_REPLACES = "m.call.replaces"
@@ -90,10 +88,7 @@ object EventType {
     // Key share events
     const val ROOM_KEY_REQUEST = "m.room_key_request"
     const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
-    val ROOM_KEY_WITHHELD = StableUnstableId(
-            stable = "m.room_key.withheld",
-            unstable = "org.matrix.room_key.withheld"
-    )
+    val ROOM_KEY_WITHHELD = StableUnstableId(stable = "m.room_key.withheld", unstable = "org.matrix.room_key.withheld")
 
     const val REQUEST_SECRET = "m.secret.request"
     const val SEND_SECRET = "m.secret.send"
@@ -111,9 +106,9 @@ object EventType {
     const val REACTION = "m.reaction"
 
     // Poll
-    val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start")
-    val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response")
-    val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end")
+    val POLL_START = StableUnstableId(stable = "m.poll.start", unstable = "org.matrix.msc3381.poll.start")
+    val POLL_RESPONSE = StableUnstableId(stable = "m.poll.response", unstable = "org.matrix.msc3381.poll.response")
+    val POLL_END = StableUnstableId(stable = "m.poll.end", unstable = "org.matrix.msc3381.poll.end")
 
     // Unwedging
     internal const val DUMMY = "m.dummy"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b305bf19b02ad4fec409eae938ea18be7e28ba13
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/ValidDecryptedEvent.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.events.model
+
+import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
+import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
+
+data class ValidDecryptedEvent(
+        val type: String,
+        val eventId: String,
+        val clearContent: Content,
+        val prevContent: Content? = null,
+        val originServerTs: Long,
+        val cryptoSenderKey: String,
+        val roomId: String,
+        val unsignedData: UnsignedData? = null,
+        val redacts: String? = null,
+        val algorithm: String,
+)
+
+fun ValidDecryptedEvent.getRelationContent(): RelationDefaultContent? {
+    return clearContent.toModel<MessageRelationContent?>()?.relatesTo
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
index cd8acbccccb3eeecc82f5d9fddd62c4ef783c68b..93208be27b9fa0774b129eba8f9caee9a91bfd2a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
@@ -47,10 +47,9 @@ interface LocationSharingService {
     /**
      * Starts sharing live location in the room.
      * @param timeoutMillis timeout of the live in milliseconds
-     * @param description description of the live for text fallback
      * @return the result of the update of the live
      */
-    suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult
+    suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult
 
     /**
      * Stops sharing live location in the room.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt
index 67bab626cbcabed9130097f5b1203bfb4899b964..7d445a5cc6df2f9829e4931d099af9bc0e86f70a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EditAggregatedSummary.kt
@@ -15,10 +15,10 @@
  */
 package org.matrix.android.sdk.api.session.room.model
 
-import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.api.session.events.model.Event
 
 data class EditAggregatedSummary(
-        val latestContent: Content? = null,
+        val latestEdit: Event? = null,
         // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
         val sourceEvents: List<String>,
         val localEchos: List<String>,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
index 5639730219e6d912c1a39f6d3e592a26a4588c20..da7e4ea9286c34162213806dd499b768f82d0dad 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReadReceipt.kt
@@ -18,5 +18,6 @@ package org.matrix.android.sdk.api.session.room.model
 
 data class ReadReceipt(
         val roomMember: RoomMemberSummary,
-        val originServerTs: Long
+        val originServerTs: Long,
+        val threadId: String?
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
index f8c1c0d79876b99b83e7f11ed930d9c0c6528511..627ce53df6fb3adc61f167fb007c124ac6bd9f89 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
@@ -34,7 +34,7 @@ data class MessageStickerContent(
          * Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
          * or some kind of content description for accessibility e.g. 'image attachment'.
          */
-        @Json(name = "body") override val body: String,
+        @Json(name = "body") override val body: String = "",
 
         /**
          * Metadata about the image referred to in url.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
index 5dcb1b4323bcf1d2edc803dafdae8cf0750d2971..b9f9335dbdc560354b4b8ffecdc07fc3033b28f6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
@@ -28,3 +28,5 @@ data class RelationDefaultContent(
 ) : RelationContent
 
 fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false
+
+fun RelationDefaultContent?.isReply(): Boolean = this?.inReplyTo?.eventId != null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt
index dac1a1a773e5c28e73c2083a2ad6799712569462..83680ec2d89f7b33c03dc5e7638a4d05ed6f8794 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt
@@ -34,12 +34,14 @@ interface ReadService {
     /**
      * Force the read marker to be set on the latest event.
      */
-    suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH)
+    suspend fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, mainTimeLineOnly: Boolean = true)
 
     /**
      * Set the read receipt on the event with provided eventId.
+     * @param eventId the id of the event where read receipt will be set
+     * @param threadId the id of the thread in which read receipt will be set. For main thread use [ReadService.THREAD_ID_MAIN] constant
      */
-    suspend fun setReadReceipt(eventId: String)
+    suspend fun setReadReceipt(eventId: String, threadId: String)
 
     /**
      * Set the read marker on the event with provided eventId.
@@ -59,10 +61,10 @@ interface ReadService {
     /**
      * Returns a live read receipt id for the room.
      */
-    fun getMyReadReceiptLive(): LiveData<Optional<String>>
+    fun getMyReadReceiptLive(threadId: String?): LiveData<Optional<String>>
 
     /**
-     * Get the eventId where the read receipt for the provided user is.
+     * Get the eventId from the main timeline where the read receipt for the provided user is.
      * @param userId the id of the user to look for
      *
      * @return the eventId where the read receipt for the provided user is attached, or null if not found
@@ -74,4 +76,8 @@ interface ReadService {
      * @param eventId the event
      */
     fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
+
+    companion object {
+        const val THREAD_ID_MAIN = "main"
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
index 8f214e0f89433ef2b5f2b104d192dde35f7fb3f2..634e71c43b5d8968adb7b7ddc0f04ecc4a8ec783 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
@@ -33,5 +33,7 @@ object RoomSummaryConstants {
             EventType.ENCRYPTED,
             EventType.STICKER,
             EventType.REACTION
-    ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
+    ) +
+            EventType.POLL_START.values +
+            EventType.STATE_ROOM_BEACON_INFO.values
 }
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 6f4049de364ba9e9a617039542e092190c7c9998..9053425a391f38b9fbdfe4e7c09bf0cfde10b2e9 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
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.timeline
 
 import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.RelationType
@@ -142,13 +143,21 @@ fun TimelineEvent.getEditedEventId(): String? {
 fun TimelineEvent.getLastMessageContent(): MessageContent? {
     return when (root.getClearType()) {
         EventType.STICKER -> root.getClearContent().toModel<MessageStickerContent>()
-        in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
-        in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconInfoContent>()
-        in EventType.BEACON_LOCATION_DATA -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>()
-        else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
+        // XXX
+        // Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing
+        // so toModel<MessageContent> won't parse them correctly
+        // It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion?
+        in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessagePollContent>()
+        in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconInfoContent>()
+        in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel<MessageBeaconLocationDataContent>()
+        else -> (getLastEditNewContent() ?: root.getClearContent()).toModel()
     }
 }
 
+fun TimelineEvent.getLastEditNewContent(): Content? {
+    return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel<MessageContent>()?.newContent
+}
+
 /**
  * Returns true if it's a reply.
  */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt
index bc592df47407b5b22969335172803409cc1c1aa1..7347bee1657e021e653bc5df44f1e3a4468b9540 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/FilterService.kt
@@ -16,19 +16,12 @@
 
 package org.matrix.android.sdk.api.session.sync
 
-interface FilterService {
-
-    enum class FilterPreset {
-        NoFilter,
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
 
-        /**
-         * Filter for Element, will include only known event type.
-         */
-        ElementFilter
-    }
+interface FilterService {
 
     /**
      * Configure the filter for the sync.
      */
-    fun setFilter(filterPreset: FilterPreset)
+    suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ad55b26dfd43b1bb7e19b6d234fd85e4ecc72857
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/filter/SyncFilterBuilder.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.sync.filter
+
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.internal.session.filter.Filter
+import org.matrix.android.sdk.internal.session.filter.RoomEventFilter
+import org.matrix.android.sdk.internal.session.filter.RoomFilter
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+
+class SyncFilterBuilder {
+    private var lazyLoadMembersForStateEvents: Boolean? = null
+    private var lazyLoadMembersForMessageEvents: Boolean? = null
+    private var useThreadNotifications: Boolean? = null
+    private var listOfSupportedEventTypes: List<String>? = null
+    private var listOfSupportedStateEventTypes: List<String>? = null
+
+    fun lazyLoadMembersForStateEvents(lazyLoadMembersForStateEvents: Boolean) = apply { this.lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents }
+
+    fun lazyLoadMembersForMessageEvents(lazyLoadMembersForMessageEvents: Boolean) =
+            apply { this.lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents }
+
+    fun useThreadNotifications(useThreadNotifications: Boolean) =
+            apply { this.useThreadNotifications = useThreadNotifications }
+
+    fun listOfSupportedStateEventTypes(listOfSupportedStateEventTypes: List<String>) =
+            apply { this.listOfSupportedStateEventTypes = listOfSupportedStateEventTypes }
+
+    fun listOfSupportedTimelineEventTypes(listOfSupportedEventTypes: List<String>) =
+            apply { this.listOfSupportedEventTypes = listOfSupportedEventTypes }
+
+    internal fun with(currentFilterParams: SyncFilterParams?) =
+            apply {
+                currentFilterParams?.let {
+                    useThreadNotifications = currentFilterParams.useThreadNotifications
+                    lazyLoadMembersForMessageEvents = currentFilterParams.lazyLoadMembersForMessageEvents
+                    lazyLoadMembersForStateEvents = currentFilterParams.lazyLoadMembersForStateEvents
+                    listOfSupportedEventTypes = currentFilterParams.listOfSupportedEventTypes?.toList()
+                    listOfSupportedStateEventTypes = currentFilterParams.listOfSupportedStateEventTypes?.toList()
+                }
+            }
+
+    internal fun extractParams(): SyncFilterParams {
+        return SyncFilterParams(
+                useThreadNotifications = useThreadNotifications,
+                lazyLoadMembersForMessageEvents = lazyLoadMembersForMessageEvents,
+                lazyLoadMembersForStateEvents = lazyLoadMembersForStateEvents,
+                listOfSupportedEventTypes = listOfSupportedEventTypes,
+                listOfSupportedStateEventTypes = listOfSupportedStateEventTypes,
+        )
+    }
+
+    internal fun build(homeServerCapabilities: HomeServerCapabilities): Filter {
+        return Filter(
+                room = buildRoomFilter(homeServerCapabilities)
+        )
+    }
+
+    private fun buildRoomFilter(homeServerCapabilities: HomeServerCapabilities): RoomFilter {
+        return RoomFilter(
+                timeline = buildTimelineFilter(homeServerCapabilities),
+                state = buildStateFilter()
+        )
+    }
+
+    private fun buildTimelineFilter(homeServerCapabilities: HomeServerCapabilities): RoomEventFilter? {
+        val resolvedUseThreadNotifications = if (homeServerCapabilities.canUseThreadReadReceiptsAndNotifications) {
+            useThreadNotifications
+        } else {
+            null
+        }
+        return RoomEventFilter(
+                enableUnreadThreadNotifications = resolvedUseThreadNotifications,
+                lazyLoadMembers = lazyLoadMembersForMessageEvents
+        ).orNullIfEmpty()
+    }
+
+    private fun buildStateFilter(): RoomEventFilter? =
+            RoomEventFilter(
+                    lazyLoadMembers = lazyLoadMembersForStateEvents,
+                    types = listOfSupportedStateEventTypes
+            ).orNullIfEmpty()
+
+    private fun RoomEventFilter.orNullIfEmpty(): RoomEventFilter? {
+        return if (hasData()) {
+            this
+        } else {
+            null
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as SyncFilterBuilder
+
+        if (lazyLoadMembersForStateEvents != other.lazyLoadMembersForStateEvents) return false
+        if (lazyLoadMembersForMessageEvents != other.lazyLoadMembersForMessageEvents) return false
+        if (useThreadNotifications != other.useThreadNotifications) return false
+        if (listOfSupportedEventTypes != other.listOfSupportedEventTypes) return false
+        if (listOfSupportedStateEventTypes != other.listOfSupportedStateEventTypes) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = lazyLoadMembersForStateEvents?.hashCode() ?: 0
+        result = 31 * result + (lazyLoadMembersForMessageEvents?.hashCode() ?: 0)
+        result = 31 * result + (useThreadNotifications?.hashCode() ?: 0)
+        result = 31 * result + (listOfSupportedEventTypes?.hashCode() ?: 0)
+        result = 31 * result + (listOfSupportedStateEventTypes?.hashCode() ?: 0)
+        return result
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt
index 3218b9994839f72d5d01c150881bf8928b335f16..0f29404d4fd10593033ace9924d0fe3baed3700f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/CrossSigningOlm.kt
@@ -83,9 +83,7 @@ internal class CrossSigningOlm @Inject constructor(
         val signaturesMadeByMyKey = signatures[myUserID] // Signatures made by me
                 ?.get("ed25519:$pubKey")
 
-        if (signaturesMadeByMyKey.isNullOrBlank()) {
-            throw IllegalArgumentException("Not signed with my key $type")
-        }
+        require(signaturesMadeByMyKey.orEmpty().isNotBlank()) { "Not signed with my key $type" }
 
         // Check that Alice USK signature of Bob MSK is valid
         olmUtility.verifyEd25519Signature(signaturesMadeByMyKey, pubKey, JsonCanonicalizer.getCanonicalJson(Map::class.java, signable))
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
index f93da745073ded51382476c3c1eaa04b89c41732..5d2797a6af42b3936836197dc628664ab3d63026 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
@@ -47,9 +47,8 @@ internal class DefaultEncryptEventTask @Inject constructor(
         // don't want to wait for any query
         // if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event
         val localEvent = params.event
-        if (localEvent.eventId == null || localEvent.type == null) {
-            throw IllegalArgumentException()
-        }
+        require(localEvent.eventId != null)
+        require(localEvent.type != null)
 
         localEchoRepository.updateSendState(localEvent.eventId, localEvent.roomId, SendState.ENCRYPTING)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
index 1a04ee030249fabee11171d0f1607a11091052ba..5b400aa63f42d7618e9c5ef7eb064b66b7c42e58 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
@@ -1140,28 +1140,25 @@ internal class DefaultVerificationService @Inject constructor(
     override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceId: String, transactionId: String?): String? {
         val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId)
         // should check if already one (and cancel it)
-        if (method == VerificationMethod.SAS) {
-            val tx = DefaultOutgoingSASDefaultVerificationTransaction(
-                    setDeviceVerificationAction,
-                    userId,
-                    deviceId,
-                    cryptoStore,
-                    crossSigningService,
-                    outgoingKeyRequestManager,
-                    secretShareManager,
-                    myDeviceInfoHolder.get().myDevice.fingerprint()!!,
-                    txID,
-                    otherUserId,
-                    otherDeviceId
-            )
-            tx.transport = verificationTransportToDeviceFactory.createTransport(tx)
-            addTransaction(tx)
+        require(method == VerificationMethod.SAS) { "Unknown verification method" }
+        val tx = DefaultOutgoingSASDefaultVerificationTransaction(
+                setDeviceVerificationAction,
+                userId,
+                deviceId,
+                cryptoStore,
+                crossSigningService,
+                outgoingKeyRequestManager,
+                secretShareManager,
+                myDeviceInfoHolder.get().myDevice.fingerprint()!!,
+                txID,
+                otherUserId,
+                otherDeviceId
+        )
+        tx.transport = verificationTransportToDeviceFactory.createTransport(tx)
+        addTransaction(tx)
 
-            tx.start()
-            return txID
-        } else {
-            throw IllegalArgumentException("Unknown verification method")
-        }
+        tx.start()
+        return txID
     }
 
     override fun requestKeyVerificationInDMs(
@@ -1343,28 +1340,25 @@ internal class DefaultVerificationService @Inject constructor(
             otherUserId: String,
             otherDeviceId: String
     ): String {
-        if (method == VerificationMethod.SAS) {
-            val tx = DefaultOutgoingSASDefaultVerificationTransaction(
-                    setDeviceVerificationAction,
-                    userId,
-                    deviceId,
-                    cryptoStore,
-                    crossSigningService,
-                    outgoingKeyRequestManager,
-                    secretShareManager,
-                    myDeviceInfoHolder.get().myDevice.fingerprint()!!,
-                    transactionId,
-                    otherUserId,
-                    otherDeviceId
-            )
-            tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
-            addTransaction(tx)
+        require(method == VerificationMethod.SAS) { "Unknown verification method" }
+        val tx = DefaultOutgoingSASDefaultVerificationTransaction(
+                setDeviceVerificationAction,
+                userId,
+                deviceId,
+                cryptoStore,
+                crossSigningService,
+                outgoingKeyRequestManager,
+                secretShareManager,
+                myDeviceInfoHolder.get().myDevice.fingerprint()!!,
+                transactionId,
+                otherUserId,
+                otherDeviceId
+        )
+        tx.transport = verificationTransportRoomMessageFactory.createTransport(roomId, tx)
+        addTransaction(tx)
 
-            tx.start()
-            return transactionId
-        } else {
-            throw IllegalArgumentException("Unknown verification method")
-        }
+        tx.start()
+        return transactionId
     }
 
     override fun readyPendingVerificationInDMs(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
index 7d263f19372f81e9dc2b3513000cde37191d9790..a1ea88a70c567836854d0dfaa45f90c7f0e25f0f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
@@ -26,17 +26,17 @@ import kotlinx.coroutines.withContext
 import timber.log.Timber
 import kotlin.system.measureTimeMillis
 
-internal fun <T> CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) {
+internal fun <T> CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: (realm: Realm) -> T) {
     asyncTransaction(monarchy.realmConfiguration, transaction)
 }
 
-internal fun <T> CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: suspend (realm: Realm) -> T) {
+internal fun <T> CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: (realm: Realm) -> T) {
     launch {
         awaitTransaction(realmConfiguration, transaction)
     }
 }
 
-internal suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T {
+internal suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: (realm: Realm) -> T): T {
     return withContext(Realm.WRITE_EXECUTOR.asCoroutineDispatcher()) {
         Realm.getInstance(config).use { bgRealm ->
             bgRealm.beginTransaction()
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 30836c027ea60291c8b1a06e7f4d30d482a0b22c..2fb87ca87412327b5addccd1497d7cf679e5d81f 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
@@ -59,6 +59,9 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045
 import org.matrix.android.sdk.internal.util.Normalizer
 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import javax.inject.Inject
@@ -67,7 +70,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
         private val normalizer: Normalizer
 ) : MatrixRealmMigration(
         dbName = "Session",
-        schemaVersion = 42L,
+        schemaVersion = 45L,
 ) {
     /**
      * Forces all RealmSessionStoreMigration instances to be equal.
@@ -119,5 +122,8 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 40) MigrateSessionTo040(realm).perform()
         if (oldVersion < 41) MigrateSessionTo041(realm).perform()
         if (oldVersion < 42) MigrateSessionTo042(realm).perform()
+        if (oldVersion < 43) MigrateSessionTo043(realm).perform()
+        if (oldVersion < 44) MigrateSessionTo044(realm).perform()
+        if (oldVersion < 45) MigrateSessionTo045(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
index 221abe0df50d6048bc58fe014f46d26a51185a49..43f84e771aba6708cbc4433bde4e18a8c52ad8cb 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
@@ -83,7 +83,6 @@ internal fun ChunkEntity.addTimelineEvent(
         this.eventId = eventId
         this.roomId = roomId
         this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
-                ?.also { it.cleanUp(eventEntity.sender) }
         this.readReceipts = readReceiptsSummaryEntity
         this.displayIndex = displayIndex
         this.ownedByThreadChunk = ownedByThreadChunk
@@ -133,7 +132,7 @@ private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventE
     val originServerTs = eventEntity.originServerTs
     if (originServerTs != null) {
         val timestampOfEvent = originServerTs.toDouble()
-        val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId)
+        val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId, threadId = eventEntity.rootThreadEventId)
         // If the synced RR is older, update
         if (timestampOfEvent > readReceiptOfSender.originServerTs) {
             val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index dfac7f670878aa1cfa298809395022f1032f6576..7999a2ea1405505f44f8f13b4c0b1887e7fea0dd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -65,11 +65,11 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
                     inThreadMessages = inThreadMessages,
                     latestMessageTimelineEventEntity = latestEventInThread
             )
-        }
-    }
 
-    if (shouldUpdateNotifications) {
-        updateNotificationsNew(roomId, realm, currentUserId)
+            if (shouldUpdateNotifications) {
+                updateThreadNotifications(roomId, realm, currentUserId, rootThreadEventId)
+            }
+        }
     }
 }
 
@@ -273,8 +273,8 @@ internal fun TimelineEventEntity.Companion.isUserMentionedInThread(realm: Realm,
 /**
  * Find the read receipt for the current user.
  */
-internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String): String? =
-        ReadReceiptEntity.where(realm, roomId = roomId, userId = userId)
+internal fun findMyReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): String? =
+        ReadReceiptEntity.where(realm, roomId = roomId, userId = userId, threadId = threadId)
                 .findFirst()
                 ?.eventId
 
@@ -293,28 +293,29 @@ internal fun isUserMentioned(currentUserId: String, timelineEventEntity: Timelin
  * Important: It will work only with the latest chunk, while read marker will be changed
  * immediately so we should not display wrong notifications
  */
-internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId: String) {
-    val readReceipt = findMyReadReceipt(realm, roomId, currentUserId) ?: return
+internal fun updateThreadNotifications(roomId: String, realm: Realm, currentUserId: String, rootThreadEventId: String) {
+    val readReceipt = findMyReadReceipt(realm, roomId, currentUserId, threadId = rootThreadEventId) ?: return
 
     val readReceiptChunk = ChunkEntity
             .findIncludingEvent(realm, readReceipt) ?: return
 
-    val readReceiptChunkTimelineEvents = readReceiptChunk
+    val readReceiptChunkThreadEvents = readReceiptChunk
             .timelineEvents
             .where()
             .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
+            .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
             .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
             .findAll() ?: return
 
-    val readReceiptChunkPosition = readReceiptChunkTimelineEvents.indexOfFirst { it.eventId == readReceipt }
+    val readReceiptChunkPosition = readReceiptChunkThreadEvents.indexOfFirst { it.eventId == readReceipt }
 
     if (readReceiptChunkPosition == -1) return
 
-    if (readReceiptChunkPosition < readReceiptChunkTimelineEvents.lastIndex) {
+    if (readReceiptChunkPosition < readReceiptChunkThreadEvents.lastIndex) {
         // If the read receipt is found inside the chunk
 
-        val threadEventsAfterReadReceipt = readReceiptChunkTimelineEvents
-                .slice(readReceiptChunkPosition..readReceiptChunkTimelineEvents.lastIndex)
+        val threadEventsAfterReadReceipt = readReceiptChunkThreadEvents
+                .slice(readReceiptChunkPosition..readReceiptChunkThreadEvents.lastIndex)
                 .filter { it.root?.isThread() == true }
 
         // In order for the below code to work for old events, we should save the previous read receipt
@@ -343,26 +344,21 @@ internal fun updateNotificationsNew(roomId: String, realm: Realm, currentUserId:
                     it.root?.rootThreadEventId
                 }
 
-        // Find the root events in the new thread events
-        val rootThreads = threadEventsAfterReadReceipt.distinctBy { it.root?.rootThreadEventId }.mapNotNull { it.root?.rootThreadEventId }
-
-        // Update root thread events only if the user have participated in
-        rootThreads.forEach { eventId ->
-            val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
-                    realm = realm,
-                    roomId = roomId,
-                    rootThreadEventId = eventId,
-                    senderId = currentUserId
-            )
-            val rootThreadEventEntity = EventEntity.where(realm, eventId).findFirst()
-
-            if (isUserParticipating) {
-                rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
-            }
+        // Update root thread event only if the user have participated in
+        val isUserParticipating = TimelineEventEntity.isUserParticipatingInThread(
+                realm = realm,
+                roomId = roomId,
+                rootThreadEventId = rootThreadEventId,
+                senderId = currentUserId
+        )
+        val rootThreadEventEntity = EventEntity.where(realm, rootThreadEventId).findFirst()
+
+        if (isUserParticipating) {
+            rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_MESSAGE
+        }
 
-            if (userMentionsList.contains(eventId)) {
-                rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
-            }
+        if (userMentionsList.contains(rootThreadEventId)) {
+            rootThreadEventEntity?.threadNotificationState = ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
index 193710f9621fc0b6e562561054f4f8be5d6d2550..0ac8dc7902b48694e8ba608f337f02de357db8d2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
@@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.database.helper
 import io.realm.Realm
 import io.realm.RealmQuery
 import io.realm.Sort
-import io.realm.kotlin.createObject
 import kotlinx.coroutines.runBlocking
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@@ -103,32 +102,6 @@ internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent(
     }
 }
 
-private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap<String, RoomMemberContent?>): TimelineEventEntity {
-    val roomId = roomId
-    val eventId = eventId
-    val localId = TimelineEventEntity.nextId(realm)
-    val senderId = sender ?: ""
-
-    val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
-        this.localId = localId
-        this.root = this@toTimelineEventEntity
-        this.eventId = eventId
-        this.roomId = roomId
-        this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
-                ?.also { it.cleanUp(sender) }
-        this.ownedByThreadChunk = true  // To skip it from the original event flow
-        val roomMemberContent = roomMemberContentsByUser[senderId]
-        this.senderAvatar = roomMemberContent?.avatarUrl
-        this.senderName = roomMemberContent?.displayName
-        isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
-            computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser)
-        } else {
-            true
-        }
-    }
-    return timelineEventEntity
-}
-
 internal fun ThreadSummaryEntity.Companion.createOrUpdate(
         threadSummaryType: ThreadSummaryUpdateType,
         realm: Realm,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8c209f2f2a7fbf650327a04698ef5772f8a2eef8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapper.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.mapper
+
+import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary
+import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.EditionOfEvent
+
+internal object EditAggregatedSummaryEntityMapper {
+
+    fun map(summary: EditAggregatedSummaryEntity?): EditAggregatedSummary? {
+        summary ?: return null
+        /**
+         * The most recent event is determined by comparing origin_server_ts;
+         * if two or more replacement events have identical origin_server_ts,
+         * the event with the lexicographically largest event_id is treated as more recent.
+         */
+        val latestEdition = summary.editions.sortedWith(compareBy<EditionOfEvent> { it.timestamp }.thenBy { it.eventId })
+                .lastOrNull() ?: return null
+        val editEvent = latestEdition.event
+
+        return EditAggregatedSummary(
+                latestEdit = editEvent?.asDomain(),
+                sourceEvents = summary.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho }
+                        .map { editionOfEvent -> editionOfEvent.eventId },
+                localEchos = summary.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho }
+                        .map { editionOfEvent -> editionOfEvent.eventId },
+                lastEditTs = latestEdition.timestamp
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt
index 6bbeb17fdd021af5a9a9123c01e946999d95320e..d4bb5791a05230eb775c45e1e432f6f1d9810dec 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventAnnotationsSummaryMapper.kt
@@ -16,7 +16,6 @@
 
 package org.matrix.android.sdk.internal.database.mapper
 
-import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary
 import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
 import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedSummary
@@ -35,18 +34,7 @@ internal object EventAnnotationsSummaryMapper {
                             it.sourceLocalEcho.toList()
                     )
                 },
-                editSummary = annotationsSummary.editSummary
-                        ?.let {
-                            val latestEdition = it.editions.maxByOrNull { editionOfEvent -> editionOfEvent.timestamp } ?: return@let null
-                            EditAggregatedSummary(
-                                    latestContent = ContentMapper.map(latestEdition.content),
-                                    sourceEvents = it.editions.filter { editionOfEvent -> !editionOfEvent.isLocalEcho }
-                                            .map { editionOfEvent -> editionOfEvent.eventId },
-                                    localEchos = it.editions.filter { editionOfEvent -> editionOfEvent.isLocalEcho }
-                                            .map { editionOfEvent -> editionOfEvent.eventId },
-                                    lastEditTs = latestEdition.timestamp
-                            )
-                        },
+                editSummary = EditAggregatedSummaryEntityMapper.map(annotationsSummary.editSummary),
                 referencesAggregatedSummary = annotationsSummary.referencesSummaryEntity?.let {
                     ReferencesAggregatedSummary(
                             ContentMapper.map(it.content),
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..645cb41af5c78157d51a8fcac008923b9f4df976
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/FilterParamsMapper.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.mapper
+
+import io.realm.RealmList
+import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+import javax.inject.Inject
+
+internal class FilterParamsMapper @Inject constructor() {
+
+    fun map(entity: SyncFilterParamsEntity): SyncFilterParams {
+        val eventTypes = if (entity.listOfSupportedEventTypesHasBeenSet) {
+            entity.listOfSupportedEventTypes?.toList()
+        } else {
+            null
+        }
+        val stateEventTypes = if (entity.listOfSupportedStateEventTypesHasBeenSet) {
+            entity.listOfSupportedStateEventTypes?.toList()
+        } else {
+            null
+        }
+        return SyncFilterParams(
+                useThreadNotifications = entity.useThreadNotifications,
+                lazyLoadMembersForMessageEvents = entity.lazyLoadMembersForMessageEvents,
+                lazyLoadMembersForStateEvents = entity.lazyLoadMembersForStateEvents,
+                listOfSupportedEventTypes = eventTypes,
+                listOfSupportedStateEventTypes = stateEventTypes,
+        )
+    }
+
+    fun map(params: SyncFilterParams): SyncFilterParamsEntity {
+        return SyncFilterParamsEntity(
+                useThreadNotifications = params.useThreadNotifications,
+                lazyLoadMembersForMessageEvents = params.lazyLoadMembersForMessageEvents,
+                lazyLoadMembersForStateEvents = params.lazyLoadMembersForStateEvents,
+                listOfSupportedEventTypes = params.listOfSupportedEventTypes.toRealmList(),
+                listOfSupportedEventTypesHasBeenSet = params.listOfSupportedEventTypes != null,
+                listOfSupportedStateEventTypes = params.listOfSupportedStateEventTypes.toRealmList(),
+                listOfSupportedStateEventTypesHasBeenSet = params.listOfSupportedStateEventTypes != null,
+        )
+    }
+
+    private fun List<String>?.toRealmList(): RealmList<String>? {
+        return this?.toTypedArray()?.let { RealmList(*it) }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
index 2be4510b6fbe25af67bb9a4df8e20e0dad5d9b2e..3b71ae3dea29e39b45aff2f080c0033a85aba477 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ReadReceiptsSummaryMapper.kt
@@ -50,7 +50,7 @@ internal class ReadReceiptsSummaryMapper @Inject constructor(
                 .mapNotNull {
                     val roomMember = RoomMemberSummaryEntity.where(realm, roomId = it.roomId, userId = it.userId).findFirst()
                             ?: return@mapNotNull null
-                    ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong())
+                    ReadReceipt(roomMember.asDomain(), it.originServerTs.toLong(), it.threadId)
                 }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt
index b61bf7e6fa8c57e64ae059992973007e104dc086..f85a0661c2d560c0b46ee6efc17d2601f8ae5b5a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt
@@ -25,11 +25,11 @@ internal class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8
 
     override fun doMigrate(realm: DynamicRealm) {
         val editionOfEventSchema = realm.schema.create("EditionOfEvent")
-                .addField(EditionOfEventFields.CONTENT, String::class.java)
+                .addField("content", String::class.java)
                 .addField(EditionOfEventFields.EVENT_ID, String::class.java)
                 .setRequired(EditionOfEventFields.EVENT_ID, true)
-                .addField(EditionOfEventFields.SENDER_ID, String::class.java)
-                .setRequired(EditionOfEventFields.SENDER_ID, true)
+                .addField("senderId", String::class.java)
+                .setRequired("senderId", true)
                 .addField(EditionOfEventFields.TIMESTAMP, Long::class.java)
                 .addField(EditionOfEventFields.IS_LOCAL_ECHO, Boolean::class.java)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt
new file mode 100644
index 0000000000000000000000000000000000000000..49e9bac18c517e0cbb3afde663d013782c1cf279
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo043.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo043(realm: DynamicRealm) : RealmMigrator(realm, 43) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        // content(string) & senderId(string) have been removed and replaced by a link to the actual event
+        realm.schema.get("EditionOfEvent")
+                ?.addRealmObjectField(EditionOfEventFields.EVENT.`$`, realm.schema.get("EventEntity")!!)
+                ?.transform { dynamicObject ->
+                    realm.where("EventEntity")
+                            .equalTo(EventEntityFields.EVENT_ID, dynamicObject.getString(EditionOfEventFields.EVENT_ID))
+                            .equalTo(EventEntityFields.SENDER, dynamicObject.getString("senderId"))
+                            .findFirst()
+                            .let {
+                                dynamicObject.setObject(EditionOfEventFields.EVENT.`$`, it)
+                            }
+                }
+                ?.removeField("senderId")
+                ?.removeField("content")
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2d3efc8338877f9fa40b79b343110e4cb394f7d2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo044.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo044(realm: DynamicRealm) : RealmMigrator(realm, 44) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("ReadReceiptEntity")
+                ?.addField(ReadReceiptEntityFields.THREAD_ID, String::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d2b43ded28db44967fde8ac4c09b9bf5fd2adb6e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo045.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo045(realm: DynamicRealm) : RealmMigrator(realm, 45) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("SyncFilterParamsEntity")
+                .addField(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_STATE_EVENTS, Boolean::class.java)
+                .setNullable(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_STATE_EVENTS, true)
+                .addField(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_MESSAGE_EVENTS, Boolean::class.java)
+                .setNullable(SyncFilterParamsEntityFields.LAZY_LOAD_MEMBERS_FOR_MESSAGE_EVENTS, true)
+                .addField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_EVENT_TYPES_HAS_BEEN_SET, Boolean::class.java)
+                .addField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_STATE_EVENT_TYPES_HAS_BEEN_SET, Boolean::class.java)
+                .addField(SyncFilterParamsEntityFields.USE_THREAD_NOTIFICATIONS, Boolean::class.java)
+                .setNullable(SyncFilterParamsEntityFields.USE_THREAD_NOTIFICATIONS, true)
+                .addRealmListField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_EVENT_TYPES.`$`, String::class.java)
+                .addRealmListField(SyncFilterParamsEntityFields.LIST_OF_SUPPORTED_STATE_EVENT_TYPES.`$`, String::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt
index 61acd51dd46730a9e45d6a8f1bf8b3180dc7757a..7b7b90f82da89d1272cdf0558dc17c62c52d1a0e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EditAggregatedSummaryEntity.kt
@@ -32,9 +32,8 @@ internal open class EditAggregatedSummaryEntity(
 
 @RealmClass(embedded = true)
 internal open class EditionOfEvent(
-        var senderId: String = "",
         var eventId: String = "",
-        var content: String? = null,
         var timestamp: Long = 0,
-        var isLocalEcho: Boolean = false
+        var isLocalEcho: Boolean = false,
+        var event: EventEntity? = null,
 ) : RealmObject()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt
index 645998d0c029ce5b5a0ba58aaa5e95bc67efea4b..9a201ab4e81508116338abf609c361e29f5a6e0d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventAnnotationsSummaryEntity.kt
@@ -19,7 +19,6 @@ import io.realm.RealmList
 import io.realm.RealmObject
 import io.realm.annotations.PrimaryKey
 import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
-import timber.log.Timber
 
 internal open class EventAnnotationsSummaryEntity(
         @PrimaryKey
@@ -32,21 +31,6 @@ internal open class EventAnnotationsSummaryEntity(
         var liveLocationShareAggregatedSummary: LiveLocationShareAggregatedSummaryEntity? = null,
 ) : RealmObject() {
 
-    /**
-     * Cleanup undesired editions, done by users different from the originalEventSender.
-     */
-    fun cleanUp(originalEventSenderId: String?) {
-        originalEventSenderId ?: return
-
-        editSummary?.editions?.filter {
-            it.senderId != originalEventSenderId
-        }
-                ?.forEach {
-                    Timber.w("Deleting an edition from ${it.senderId} of event sent by $originalEventSenderId")
-                    it.deleteFromRealm()
-                }
-    }
-
     companion object
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt
index 9623c95359718f81dd994a17573baa6d7b310015..cedd5e742421d393180565b08c77865e050ebf27 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ReadReceiptEntity.kt
@@ -26,6 +26,7 @@ internal open class ReadReceiptEntity(
         var eventId: String = "",
         var roomId: String = "",
         var userId: String = "",
+        var threadId: String? = null,
         var originServerTs: Double = 0.0
 ) : RealmObject() {
     companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index b222bcb710babfd3753fe045848b6aed804f48ba..93ff67a911010b35a5eb655c6f02cc49fef8ec59 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
@@ -70,7 +70,8 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
             SpaceChildSummaryEntity::class,
             SpaceParentSummaryEntity::class,
             UserPresenceEntity::class,
-            ThreadSummaryEntity::class
+            ThreadSummaryEntity::class,
+            SyncFilterParamsEntity::class,
         ]
 )
 internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e4b62f28e8ae98902759de8282ae289ec12b0c70
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SyncFilterParamsEntity.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.model
+
+import io.realm.RealmList
+import io.realm.RealmObject
+
+/**
+ * This entity stores Sync Filter configuration data, provided by the client.
+ */
+internal open class SyncFilterParamsEntity(
+        var lazyLoadMembersForStateEvents: Boolean? = null,
+        var lazyLoadMembersForMessageEvents: Boolean? = null,
+        var useThreadNotifications: Boolean? = null,
+        var listOfSupportedEventTypes: RealmList<String>? = null,
+        var listOfSupportedEventTypesHasBeenSet: Boolean = false,
+        var listOfSupportedStateEventTypes: RealmList<String>? = null,
+        var listOfSupportedStateEventTypesHasBeenSet: Boolean = false,
+) : RealmObject() {
+
+    companion object
+}
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 c8f22dc2ccbe8950907c48a470b7f8223e5bd009..1deca47b70a777c998ad95ae36f392ac389ccb6b 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
@@ -20,6 +20,7 @@ import io.realm.RealmObject
 import io.realm.RealmResults
 import io.realm.annotations.Index
 import io.realm.annotations.LinkingObjects
+import org.matrix.android.sdk.api.session.room.read.ReadService
 import org.matrix.android.sdk.internal.extensions.assertIsManaged
 
 internal open class TimelineEventEntity(
@@ -52,3 +53,7 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) {
     }
     deleteFromRealm()
 }
+
+internal fun TimelineEventEntity.getThreadId(): String {
+    return root?.rootThreadEventId ?: ReadService.THREAD_ID_MAIN
+}
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 0b0f01a67de8aff906bc081d8dc7b1f7bc5bec7c..ebfe23105e351bf5752bd76c119430665189920a 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,17 +18,20 @@ 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.api.session.room.read.ReadService
 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
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.getThreadId
 
 internal fun isEventRead(
         realmConfiguration: RealmConfiguration,
         userId: String?,
         roomId: String?,
-        eventId: String?
+        eventId: String?,
+        shouldCheckIfReadInEventsThread: Boolean
 ): Boolean {
     if (userId.isNullOrBlank() || roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
         return false
@@ -45,7 +48,8 @@ internal fun isEventRead(
             eventToCheck.root?.sender == userId -> true
             // If new event exists and the latest event is from ourselves we can infer the event is read
             latestEventIsFromSelf(realm, roomId, userId) -> true
-            eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId) -> true
+            eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, null) -> true
+            (shouldCheckIfReadInEventsThread && eventToCheck.isBeforeLatestReadReceipt(realm, roomId, userId, eventToCheck.getThreadId())) -> true
             else -> false
         }
     }
@@ -54,27 +58,33 @@ internal fun isEventRead(
 private fun latestEventIsFromSelf(realm: Realm, roomId: String, userId: String) = TimelineEventEntity.latestEvent(realm, roomId, true)
         ?.root?.sender == userId
 
-private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String): Boolean {
-    return ReadReceiptEntity.where(realm, roomId, userId).findFirst()?.let { readReceipt ->
+private fun TimelineEventEntity.isBeforeLatestReadReceipt(realm: Realm, roomId: String, userId: String, threadId: String?): Boolean {
+    val isMoreRecent = ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst()?.let { readReceipt ->
         val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst()
         readReceiptEvent?.isMoreRecentThan(this)
     } ?: false
+    return isMoreRecent
 }
 
 /**
  * Missing events can be caused by the latest timeline chunk no longer contain an older event or
  * by fast lane eagerly displaying events before the database has finished updating.
  */
-private fun hasReadMissingEvent(realm: Realm, latestChunkEntity: ChunkEntity, roomId: String, userId: String, eventId: String): Boolean {
-    return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId)
+private fun hasReadMissingEvent(realm: Realm,
+                                latestChunkEntity: ChunkEntity,
+                                roomId: String,
+                                userId: String,
+                                eventId: String,
+                                threadId: String? = ReadService.THREAD_ID_MAIN): Boolean {
+    return realm.doesEventExistInChunkHistory(eventId) && realm.hasReadReceiptInLatestChunk(latestChunkEntity, roomId, userId, threadId)
 }
 
 private fun Realm.doesEventExistInChunkHistory(eventId: String): Boolean {
     return ChunkEntity.findIncludingEvent(this, eventId) != null
 }
 
-private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String): Boolean {
-    return ReadReceiptEntity.where(this, roomId = roomId, userId = userId).findFirst()?.let {
+private fun Realm.hasReadReceiptInLatestChunk(latestChunkEntity: ChunkEntity, roomId: String, userId: String, threadId: String?): Boolean {
+    return ReadReceiptEntity.where(this, roomId = roomId, userId = userId, threadId = threadId).findFirst()?.let {
         latestChunkEntity.timelineEvents.find(it.eventId)
     } != null
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt
index 170814d3f280ede63662e52710a8d9371bec7a70..0f9f56b938d5f0f3c72588c6b576877a70dd8fc1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadReceiptEntityQueries.kt
@@ -20,12 +20,20 @@ import io.realm.Realm
 import io.realm.RealmQuery
 import io.realm.kotlin.createObject
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.session.room.read.ReadService
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntityFields
 
-internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> {
+internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String, threadId: String?): RealmQuery<ReadReceiptEntity> {
     return realm.where<ReadReceiptEntity>()
-            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId))
+            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, threadId))
+}
+
+internal fun ReadReceiptEntity.Companion.forMainTimelineWhere(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> {
+    return realm.where<ReadReceiptEntity>()
+            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, ReadService.THREAD_ID_MAIN))
+            .or()
+            .equalTo(ReadReceiptEntityFields.PRIMARY_KEY, buildPrimaryKey(roomId, userId, null))
 }
 
 internal fun ReadReceiptEntity.Companion.whereUserId(realm: Realm, userId: String): RealmQuery<ReadReceiptEntity> {
@@ -38,23 +46,37 @@ internal fun ReadReceiptEntity.Companion.whereRoomId(realm: Realm, roomId: Strin
             .equalTo(ReadReceiptEntityFields.ROOM_ID, roomId)
 }
 
-internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId: String, userId: String, originServerTs: Double): ReadReceiptEntity {
+internal fun ReadReceiptEntity.Companion.createUnmanaged(
+        roomId: String,
+        eventId: String,
+        userId: String,
+        threadId: String?,
+        originServerTs: Double
+): ReadReceiptEntity {
     return ReadReceiptEntity().apply {
-        this.primaryKey = "${roomId}_$userId"
+        this.primaryKey = buildPrimaryKey(roomId, userId, threadId)
         this.eventId = eventId
         this.roomId = roomId
         this.userId = userId
+        this.threadId = threadId
         this.originServerTs = originServerTs
     }
 }
 
-internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity {
-    return ReadReceiptEntity.where(realm, roomId, userId).findFirst()
-            ?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId))
+internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String, threadId: String?): ReadReceiptEntity {
+    return ReadReceiptEntity.where(realm, roomId, userId, threadId).findFirst()
+            ?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId, threadId))
                     .apply {
                         this.roomId = roomId
                         this.userId = userId
+                        this.threadId = threadId
                     }
 }
 
-private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId"
+private fun buildPrimaryKey(roomId: String, userId: String, threadId: String?): String {
+    return if (threadId == null) {
+        "${roomId}_${userId}"
+    } else {
+        "${roomId}_${userId}_${threadId}"
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt
index a650fa2d64cbeb761b1aad4c190f9b2de6a2341d..9741a7bd151cdd5cace8aa15cab1bd5d0dcab739 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/EventInsertLiveProcessor.kt
@@ -24,7 +24,7 @@ internal interface EventInsertLiveProcessor {
 
     fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean
 
-    suspend fun process(realm: Realm, event: Event)
+    fun process(realm: Realm, event: Event)
 
     /**
      * Called after transaction.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
index b15a6474218db146a2204478e08fc00746000b1d..b6ad7581fecdbf7def2bda3eaebca0bfa1eed121 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
@@ -41,9 +41,8 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
             EventType.CALL_INVITE,
             EventType.CALL_HANGUP,
             EventType.ENCRYPTED,
-            EventType.CALL_ASSERTED_IDENTITY,
-            EventType.CALL_ASSERTED_IDENTITY_PREFIX
-    )
+    ) +
+            EventType.CALL_ASSERTED_IDENTITY.values
 
     private val eventsToPostProcess = mutableListOf<Event>()
 
@@ -54,7 +53,7 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
         return allowedTypes.contains(eventType)
     }
 
-    override suspend fun process(realm: Realm, event: Event) {
+    override fun process(realm: Realm, event: Event) {
         eventsToPostProcess.add(event)
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
index 48a9dfd3dafd6af055ba2fe5aab06535c485cfed..d824aaa51a1b1be4a20f329f7082a0b6bc944463 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
@@ -84,8 +84,7 @@ internal class CallSignalingHandler @Inject constructor(
             EventType.CALL_NEGOTIATE -> {
                 handleCallNegotiateEvent(event)
             }
-            EventType.CALL_ASSERTED_IDENTITY,
-            EventType.CALL_ASSERTED_IDENTITY_PREFIX -> {
+            in EventType.CALL_ASSERTED_IDENTITY.values -> {
                 handleCallAssertedIdentityEvent(event)
             }
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt
index 1d1bb0e7150b0bf484811b7dcb0a54043e7961da..4e5b005584f249e4e3bdf83a11e1937d4cca0360 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterRepository.kt
@@ -17,74 +17,71 @@
 package org.matrix.android.sdk.internal.session.filter
 
 import com.zhuinden.monarchy.Monarchy
-import io.realm.Realm
 import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.mapper.FilterParamsMapper
 import org.matrix.android.sdk.internal.database.model.FilterEntity
-import org.matrix.android.sdk.internal.database.model.FilterEntityFields
+import org.matrix.android.sdk.internal.database.model.SyncFilterParamsEntity
 import org.matrix.android.sdk.internal.database.query.get
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import javax.inject.Inject
 
-internal class DefaultFilterRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) : FilterRepository {
+internal class DefaultFilterRepository @Inject constructor(
+        @SessionDatabase private val monarchy: Monarchy,
+        private val filterParamsMapper: FilterParamsMapper
+) : FilterRepository {
 
-    override suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean {
-        return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
-            val filterEntity = FilterEntity.get(realm)
-            // Filter has changed, or no filter Id yet
-            filterEntity == null ||
-                    filterEntity.filterBodyJson != filter.toJSONString() ||
-                    filterEntity.filterId.isBlank()
-        }.also { hasChanged ->
-            if (hasChanged) {
-                // Filter is new or has changed, store it and reset the filter Id.
-                // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread
-                monarchy.awaitTransaction { realm ->
-                    // We manage only one filter for now
-                    val filterJson = filter.toJSONString()
-                    val roomEventFilterJson = roomEventFilter.toJSONString()
+    override suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter) {
+        monarchy.awaitTransaction { realm ->
+            // We manage only one filter for now
+            val filterJson = filter.toJSONString()
+            val roomEventFilterJson = roomEventFilter.toJSONString()
 
-                    val filterEntity = FilterEntity.getOrCreate(realm)
+            val filterEntity = FilterEntity.getOrCreate(realm)
 
-                    filterEntity.filterBodyJson = filterJson
-                    filterEntity.roomEventFilterJson = roomEventFilterJson
-                    // Reset filterId
-                    filterEntity.filterId = ""
-                }
-            }
+            filterEntity.filterBodyJson = filterJson
+            filterEntity.roomEventFilterJson = roomEventFilterJson
+            filterEntity.filterId = filterId
         }
     }
 
-    override suspend fun storeFilterId(filter: Filter, filterId: String) {
-        monarchy.awaitTransaction {
-            // We manage only one filter for now
-            val filterJson = filter.toJSONString()
-
-            // Update the filter id, only if the filter body matches
-            it.where<FilterEntity>()
-                    .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson)
-                    ?.findFirst()
-                    ?.filterId = filterId
+    override suspend fun getStoredSyncFilterBody(): String {
+        return monarchy.awaitTransaction {
+            FilterEntity.getOrCreate(it).filterBodyJson
         }
     }
 
-    override suspend fun getFilter(): String {
+    override suspend fun getStoredSyncFilterId(): String? {
         return monarchy.awaitTransaction {
-            val filter = FilterEntity.getOrCreate(it)
-            if (filter.filterId.isBlank()) {
-                // Use the Json format
-                filter.filterBodyJson
+            val id = FilterEntity.get(it)?.filterId
+            if (id.isNullOrBlank()) {
+                null
             } else {
-                // Use FilterId
-                filter.filterId
+                id
             }
         }
     }
 
-    override suspend fun getRoomFilter(): String {
+    override suspend fun getRoomFilterBody(): String {
         return monarchy.awaitTransaction {
             FilterEntity.getOrCreate(it).roomEventFilterJson
         }
     }
+
+    override suspend fun getStoredFilterParams(): SyncFilterParams? {
+        return monarchy.awaitTransaction { realm ->
+            realm.where<SyncFilterParamsEntity>().findFirst()?.let {
+                filterParamsMapper.map(it)
+            }
+        }
+    }
+
+    override suspend fun storeFilterParams(params: SyncFilterParams) {
+        return monarchy.awaitTransaction { realm ->
+            val entity = filterParamsMapper.map(params)
+            realm.insertOrUpdate(entity)
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt
index 2e68d02d8ce6b6427e6c23bff27b209ea0f219ba..c54e7de07aec15538cad8f49ad3a33371e743bf2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/DefaultFilterService.kt
@@ -17,19 +17,27 @@
 package org.matrix.android.sdk.internal.session.filter
 
 import org.matrix.android.sdk.api.session.sync.FilterService
-import org.matrix.android.sdk.internal.task.TaskExecutor
-import org.matrix.android.sdk.internal.task.configureWith
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
+import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
 import javax.inject.Inject
 
 internal class DefaultFilterService @Inject constructor(
         private val saveFilterTask: SaveFilterTask,
-        private val taskExecutor: TaskExecutor
+        private val filterRepository: FilterRepository,
+        private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
 ) : FilterService {
 
     // TODO Pass a list of support events instead
-    override fun setFilter(filterPreset: FilterService.FilterPreset) {
-        saveFilterTask
-                .configureWith(SaveFilterTask.Params(filterPreset))
-                .executeBy(taskExecutor)
+    override suspend fun setSyncFilter(filterBuilder: SyncFilterBuilder) {
+        filterRepository.storeFilterParams(filterBuilder.extractParams())
+
+        // don't upload/store filter until homeserver capabilities are fetched
+        homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.let { homeServerCapabilities ->
+            saveFilterTask.execute(
+                    SaveFilterTask.Params(
+                            filter = filterBuilder.build(homeServerCapabilities)
+                    )
+            )
+        }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
index e0919c52e3e9c49aff4f0a962d2aa2a69ae76c98..1bd2e59e59ac109c66595dc04a6c0c610d9a7adc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
@@ -45,46 +45,7 @@ internal object FilterFactory {
         return FilterUtil.enableLazyLoading(Filter(), true)
     }
 
-    fun createElementFilter(): Filter {
-        return Filter(
-                room = RoomFilter(
-                        timeline = createElementTimelineFilter(),
-                        state = createElementStateFilter()
-                )
-        )
-    }
-
     fun createDefaultRoomFilter(): RoomEventFilter {
         return RoomEventFilter(lazyLoadMembers = true)
     }
-
-    fun createElementRoomFilter(): RoomEventFilter {
-        return RoomEventFilter(
-                lazyLoadMembers = true,
-                // TODO Enable this for optimization
-                // types = (listOfSupportedEventTypes + listOfSupportedStateEventTypes).toMutableList()
-        )
-    }
-
-    private fun createElementTimelineFilter(): RoomEventFilter? {
-//        we need to check if homeserver supports thread notifications before setting this param
-//        return RoomEventFilter(enableUnreadThreadNotifications = true)
-        return null
-    }
-
-    private fun createElementStateFilter(): RoomEventFilter {
-        return RoomEventFilter(lazyLoadMembers = true)
-    }
-
-    // Get only managed types by Element
-    private val listOfSupportedEventTypes = listOf(
-            // TODO Complete the list
-            EventType.MESSAGE
-    )
-
-    // Get only managed types by Element
-    private val listOfSupportedStateEventTypes = listOf(
-            // TODO Complete the list
-            EventType.STATE_ROOM_MEMBER
-    )
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt
index 8531bed1ff318215f151e24be001c74b335d0dac..ca9f798fd96efa634d6296d03a683983e1908d8d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterModule.kt
@@ -44,4 +44,7 @@ internal abstract class FilterModule {
 
     @Binds
     abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask
+
+    @Binds
+    abstract fun bindGetCurrentFilterTask(task: DefaultGetCurrentFilterTask): GetCurrentFilterTask
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt
index f40231c8cffa93d7f1bf44828143867685b640bf..71d7391e87cc39bd972bd6a301edeaf552e64ac9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterRepository.kt
@@ -16,25 +16,42 @@
 
 package org.matrix.android.sdk.internal.session.filter
 
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+
+/**
+ * Repository for request filters.
+ */
 internal interface FilterRepository {
 
     /**
-     * Return true if the filterBody has changed, or need to be sent to the server.
+     * Stores sync filter and room filter.
+     * Note: It looks like we could use [Filter.room.timeline] instead of a separate [RoomEventFilter], but it's not clear if it's safe, so research is needed
+     * @return true if the filterBody has changed, or need to be sent to the server.
      */
-    suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean
+    suspend fun storeSyncFilter(filter: Filter, filterId: String, roomEventFilter: RoomEventFilter)
 
     /**
-     * Set the filterId of this filter.
+     * Returns stored sync filter's JSON body if it exists.
      */
-    suspend fun storeFilterId(filter: Filter, filterId: String)
+    suspend fun getStoredSyncFilterBody(): String?
 
     /**
-     * Return filter json or filter id.
+     * Returns stored sync filter's ID if it exists.
      */
-    suspend fun getFilter(): String
+    suspend fun getStoredSyncFilterId(): String?
 
     /**
      * Return the room filter.
      */
-    suspend fun getRoomFilter(): String
+    suspend fun getRoomFilterBody(): String
+
+    /**
+     * Returns filter params stored in local storage if it exists.
+     */
+    suspend fun getStoredFilterParams(): SyncFilterParams?
+
+    /**
+     * Stores filter params to local storage.
+     */
+    suspend fun storeFilterParams(params: SyncFilterParams)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..76805c5c514ab8b3d88166fc7541cde278b23043
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/GetCurrentFilterTask.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.filter
+
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
+import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface GetCurrentFilterTask : Task<Unit, String>
+
+internal class DefaultGetCurrentFilterTask @Inject constructor(
+        private val filterRepository: FilterRepository,
+        private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource,
+        private val saveFilterTask: SaveFilterTask
+) : GetCurrentFilterTask {
+
+    override suspend fun execute(params: Unit): String {
+        val storedFilterId = filterRepository.getStoredSyncFilterId()
+        val storedFilterBody = filterRepository.getStoredSyncFilterBody()
+        val homeServerCapabilities = homeServerCapabilitiesDataSource.getHomeServerCapabilities() ?: HomeServerCapabilities()
+        val currentFilter = SyncFilterBuilder()
+                .with(filterRepository.getStoredFilterParams())
+                .build(homeServerCapabilities)
+
+        val currentFilterBody = currentFilter.toJSONString()
+
+        return when (storedFilterBody) {
+            currentFilterBody -> storedFilterId ?: storedFilterBody
+            else -> saveFilter(currentFilter) ?: currentFilterBody
+        }
+    }
+
+    private suspend fun saveFilter(filter: Filter) = saveFilterTask
+            .execute(
+                    SaveFilterTask.Params(
+                            filter = filter
+                    )
+            )
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
index 63afa1bbbc4a1bdf1c0dba08e1121caab3b44de8..0223cd3ee74ee99f3bcfdce4b66d6109e1786d26 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/SaveFilterTask.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.filter
 
-import org.matrix.android.sdk.api.session.sync.FilterService
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
@@ -25,11 +25,12 @@ import javax.inject.Inject
 
 /**
  * Save a filter, in db and if any changes, upload to the server.
+ * Return the filterId if uploading to the server is successful, else return null.
  */
-internal interface SaveFilterTask : Task<SaveFilterTask.Params, Unit> {
+internal interface SaveFilterTask : Task<SaveFilterTask.Params, String?> {
 
     data class Params(
-            val filterPreset: FilterService.FilterPreset
+            val filter: Filter
     )
 }
 
@@ -37,33 +38,23 @@ internal class DefaultSaveFilterTask @Inject constructor(
         @UserId private val userId: String,
         private val filterAPI: FilterApi,
         private val filterRepository: FilterRepository,
-        private val globalErrorReceiver: GlobalErrorReceiver
+        private val globalErrorReceiver: GlobalErrorReceiver,
 ) : SaveFilterTask {
 
-    override suspend fun execute(params: SaveFilterTask.Params) {
-        val filterBody = when (params.filterPreset) {
-            FilterService.FilterPreset.ElementFilter -> {
-                FilterFactory.createElementFilter()
-            }
-            FilterService.FilterPreset.NoFilter -> {
-                FilterFactory.createDefaultFilter()
-            }
-        }
-        val roomFilter = when (params.filterPreset) {
-            FilterService.FilterPreset.ElementFilter -> {
-                FilterFactory.createElementRoomFilter()
-            }
-            FilterService.FilterPreset.NoFilter -> {
-                FilterFactory.createDefaultRoomFilter()
+    override suspend fun execute(params: SaveFilterTask.Params): String? {
+        val filter = params.filter
+        val filterResponse = tryOrNull {
+            executeRequest(globalErrorReceiver) {
+                filterAPI.uploadFilter(userId, filter)
             }
         }
-        val updated = filterRepository.storeFilter(filterBody, roomFilter)
-        if (updated) {
-            val filterResponse = executeRequest(globalErrorReceiver) {
-                // TODO auto retry
-                filterAPI.uploadFilter(userId, filterBody)
-            }
-            filterRepository.storeFilterId(filterBody, filterResponse.filterId)
-        }
+
+        val filterId = filterResponse?.filterId
+        filterRepository.storeSyncFilter(
+                filter = filter,
+                filterId = filterId.orEmpty(),
+                roomEventFilter = FilterFactory.createDefaultRoomFilter()
+        )
+        return filterId
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt
index 09d7d50ecb86091eb0d2bdfa20f59e1f2bf7282b..9fe93d8262c0450e42080852e06e1301c5ebe07b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt
@@ -56,8 +56,8 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
 
         val allEvents = (newJoinEvents + inviteEvents).filter { event ->
             when (event.type) {
-                in EventType.POLL_START,
-                in EventType.STATE_ROOM_BEACON_INFO,
+                in EventType.POLL_START.values,
+                in EventType.STATE_ROOM_BEACON_INFO.values,
                 EventType.MESSAGE,
                 EventType.REDACTION,
                 EventType.ENCRYPTED,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..41d0c3f6ab1554989c33a535021cc2ba79e7d7a9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room
+
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.LocalEcho
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import org.matrix.android.sdk.api.session.events.model.getRelationContent
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import timber.log.Timber
+import javax.inject.Inject
+
+internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCryptoStore) {
+
+    sealed class EditValidity {
+        object Valid : EditValidity()
+        data class Invalid(val reason: String) : EditValidity()
+        object Unknown : EditValidity()
+    }
+
+    /**
+     * There are a number of requirements on replacement events, which must be satisfied for the replacement
+     * to be considered valid:
+     * As with all event relationships, the original event and replacement event must have the same room_id
+     * (i.e. you cannot send an event in one room and then an edited version in a different room).
+     * The original event and replacement event must have the same sender (i.e. you cannot edit someone else’s messages).
+     * The replacement and original events must have the same type (i.e. you cannot change the original event’s type).
+     * The replacement and original events must not have a state_key property (i.e. you cannot edit state events at all).
+     * The original event must not, itself, have a rel_type of m.replace
+     * (i.e. you cannot edit an edit — though you can send multiple edits for a single original event).
+     * The replacement event (once decrypted, if appropriate) must have an m.new_content property.
+     *
+     * If the original event was encrypted, the replacement should be too.
+     */
+    fun validateEdit(originalEvent: Event?, replaceEvent: Event): EditValidity {
+        Timber.v("###REPLACE valide event $originalEvent replaced $replaceEvent")
+        // we might not know the original event at that time. In this case we can't perform the validation
+        // Edits should be revalidated when the original event is received
+        if (originalEvent == null) {
+            return EditValidity.Unknown
+        }
+
+        if (LocalEcho.isLocalEchoId(replaceEvent.eventId.orEmpty())) {
+            // Don't validate local echo
+            return EditValidity.Unknown
+        }
+
+        if (originalEvent.roomId != replaceEvent.roomId) {
+            return EditValidity.Invalid("original event and replacement event must have the same room_id")
+        }
+        if (originalEvent.isStateEvent() || replaceEvent.isStateEvent()) {
+            return EditValidity.Invalid("replacement and original events must not have a state_key property")
+        }
+        // check it's from same sender
+
+        if (originalEvent.isEncrypted()) {
+            if (!replaceEvent.isEncrypted()) return EditValidity.Invalid("If the original event was encrypted, the replacement should be too")
+            val originalDecrypted = originalEvent.toValidDecryptedEvent()
+                    ?: return EditValidity.Unknown // UTD can't decide
+            val replaceDecrypted = replaceEvent.toValidDecryptedEvent()
+                    ?: return EditValidity.Unknown // UTD can't decide
+
+            val originalCryptoSenderId = cryptoStore.deviceWithIdentityKey(originalDecrypted.cryptoSenderKey)?.userId
+            val editCryptoSenderId = cryptoStore.deviceWithIdentityKey(replaceDecrypted.cryptoSenderKey)?.userId
+
+            if (originalDecrypted.getRelationContent()?.type == RelationType.REPLACE) {
+                return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ")
+            }
+
+            if (originalCryptoSenderId == null || editCryptoSenderId == null) {
+                // mm what can we do? we don't know if it's cryptographically from same user?
+                // let valid and UI should display send by deleted device warning?
+                val bestEffortOriginal = originalCryptoSenderId ?: originalEvent.senderId
+                val bestEffortEdit = editCryptoSenderId ?: replaceEvent.senderId
+                if (bestEffortOriginal != bestEffortEdit) {
+                    return EditValidity.Invalid("original event and replacement event must have the same sender")
+                }
+            } else {
+                if (originalCryptoSenderId != editCryptoSenderId) {
+                    return EditValidity.Invalid("Crypto: original event and replacement event must have the same sender")
+                }
+            }
+
+            if (originalDecrypted.type != replaceDecrypted.type) {
+                return EditValidity.Invalid("replacement and original events must have the same type")
+            }
+            if (replaceDecrypted.clearContent.toModel<MessageContent>()?.newContent == null) {
+                return EditValidity.Invalid("replacement event must have an m.new_content property")
+            }
+        } else {
+            if (originalEvent.getRelationContent()?.type == RelationType.REPLACE) {
+                return EditValidity.Invalid("The original event must not, itself, have a rel_type of m.replace ")
+            }
+
+            // check the sender
+            if (originalEvent.senderId != replaceEvent.senderId) {
+                return EditValidity.Invalid("original event and replacement event must have the same sender")
+            }
+            if (originalEvent.type != replaceEvent.type) {
+                return EditValidity.Invalid("replacement and original events must have the same type")
+            }
+            if (replaceEvent.content.toModel<MessageContent>()?.newContent == null) {
+                return EditValidity.Invalid("replacement event must have an m.new_content property")
+            }
+        }
+
+        return EditValidity.Valid
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index 24d4975eb9a905d53cfac917043dbc93c464334a..be733098370ac6dc82ab4e657a4a2b7eb1e63498 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -42,6 +42,7 @@ import org.matrix.android.sdk.internal.crypto.verification.toState
 import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
 import org.matrix.android.sdk.internal.database.mapper.ContentMapper
 import org.matrix.android.sdk.internal.database.mapper.EventMapper
+import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
 import org.matrix.android.sdk.internal.database.model.EditionOfEvent
 import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
@@ -72,6 +73,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
         private val sessionManager: SessionManager,
         private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor,
         private val pollAggregationProcessor: PollAggregationProcessor,
+        private val editValidator: EventEditValidator,
         private val clock: Clock,
 ) : EventInsertLiveProcessor {
 
@@ -79,22 +81,28 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             EventType.MESSAGE,
             EventType.REDACTION,
             EventType.REACTION,
+            // The aggregator handles verification events but just to render tiles in the timeline
+            // It's not participating in verification itself, just timeline display
             EventType.KEY_VERIFICATION_DONE,
             EventType.KEY_VERIFICATION_CANCEL,
             EventType.KEY_VERIFICATION_ACCEPT,
             EventType.KEY_VERIFICATION_START,
             EventType.KEY_VERIFICATION_MAC,
-            // TODO Add ?
-            // EventType.KEY_VERIFICATION_READY,
+            EventType.KEY_VERIFICATION_READY,
             EventType.KEY_VERIFICATION_KEY,
             EventType.ENCRYPTED
-    ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA
+    ) +
+            EventType.POLL_START.values +
+            EventType.POLL_RESPONSE.values +
+            EventType.POLL_END.values +
+            EventType.STATE_ROOM_BEACON_INFO.values +
+            EventType.BEACON_LOCATION_DATA.values
 
     override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
         return allowedTypes.contains(eventType)
     }
 
-    override suspend fun process(realm: Realm, event: Event) {
+    override fun process(realm: Realm, event: Event) {
         try { // Temporary catch, should be removed
             val roomId = event.roomId
             if (roomId == null) {
@@ -102,7 +110,31 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                 return
             }
             val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
-            when (event.type) {
+
+            // It might be a late decryption of the original event or a event received when back paginating?
+            // let's check if there is already a summary for it and do some cleaning
+            if (!isLocalEcho) {
+                EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId.orEmpty())
+                        .findFirst()
+                        ?.editSummary
+                        ?.editions
+                        ?.forEach { editionOfEvent ->
+                            EventEntity.where(realm, editionOfEvent.eventId).findFirst()?.asDomain()?.let { editEvent ->
+                                when (editValidator.validateEdit(event, editEvent)) {
+                                    is EventEditValidator.EditValidity.Invalid -> {
+                                        // delete it, it was invalid
+                                        Timber.v("## Replace: Removing a previously accepted edit for event ${event.eventId}")
+                                        editionOfEvent.deleteFromRealm()
+                                    }
+                                    else -> {
+                                        // nop
+                                    }
+                                }
+                            }
+                        }
+            }
+
+            when (event.getClearType()) {
                 EventType.REACTION -> {
                     // we got a reaction!!
                     Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
@@ -113,21 +145,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}")
                         handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
 
-                        EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
-                                ?.let {
-                                    TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
-                                            ?.forEach { tet -> tet.annotations = it }
-                                }
+                        // XXX do something for aggregated edits?
+                        // it's a bit strange as it would require to do a server query to get the edition?
                     }
 
-                    val content: MessageContent? = event.content.toModel()
-                    if (content?.relatesTo?.type == RelationType.REPLACE) {
+                    val relationContent = event.getRelationContent()
+                    if (relationContent?.type == RelationType.REPLACE) {
                         Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
                         // A replace!
-                        handleReplace(realm, event, content, roomId, isLocalEcho)
+                        handleReplace(realm, event, roomId, isLocalEcho, relationContent.eventId)
                     }
                 }
-
                 EventType.KEY_VERIFICATION_DONE,
                 EventType.KEY_VERIFICATION_CANCEL,
                 EventType.KEY_VERIFICATION_ACCEPT,
@@ -142,74 +170,31 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         }
                     }
                 }
-
+                // As for now Live event processors are not receiving UTD events.
+                // They will get an update if the event is decrypted later
                 EventType.ENCRYPTED -> {
-                    // Relation type is in clear
+                    // Relation type is in clear, it might be possible to do some things?
+                    // Notice that if the event is decrypted later, process be called again
                     val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
-                    if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE ||
-                            encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE
-                    ) {
-                        event.getClearContent().toModel<MessageContent>()?.let {
-                            if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) {
-                                Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
-                                // A replace!
-                                handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
-                            } else if (event.getClearType() in EventType.POLL_RESPONSE) {
-                                sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
-                                    pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
-                                }
-                            }
+                    when (encryptedEventContent?.relatesTo?.type) {
+                        RelationType.REPLACE -> {
+                            Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
+                            // A replace!
+                            handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
                         }
-                    } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
-                        when (event.getClearType()) {
-                            EventType.KEY_VERIFICATION_DONE,
-                            EventType.KEY_VERIFICATION_CANCEL,
-                            EventType.KEY_VERIFICATION_ACCEPT,
-                            EventType.KEY_VERIFICATION_START,
-                            EventType.KEY_VERIFICATION_MAC,
-                            EventType.KEY_VERIFICATION_READY,
-                            EventType.KEY_VERIFICATION_KEY -> {
-                                Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
-                                encryptedEventContent.relatesTo.eventId?.let {
-                                    handleVerification(realm, event, roomId, isLocalEcho, it)
-                                }
-                            }
-                            in EventType.POLL_RESPONSE -> {
-                                event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let {
-                                    sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
-                                        pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
-                                    }
-                                }
-                            }
-                            in EventType.POLL_END -> {
-                                sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
-                                    getPowerLevelsHelper(event.roomId)?.let {
-                                        pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
-                                    }
-                                }
-                            }
-                            in EventType.BEACON_LOCATION_DATA -> {
-                                handleBeaconLocationData(event, realm, roomId, isLocalEcho)
-                            }
+                        RelationType.RESPONSE -> {
+                            // can we / should we do we something for UTD response??
+                            Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
                         }
-                    } else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) {
-                        // Reaction
-                        if (event.getClearType() == EventType.REACTION) {
-                            // we got a reaction!!
-                            Timber.v("###REACTION e2e in room $roomId , reaction eventID ${event.eventId}")
-                            handleReaction(realm, event, roomId, isLocalEcho)
+                        RelationType.REFERENCE -> {
+                            // can we / should we do we something for UTD reference??
+                            Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
+                        }
+                        RelationType.ANNOTATION -> {
+                            // can we / should we do we something for UTD annotation??
+                            Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}")
                         }
                     }
-                    // HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations
-//                    else if (event.unsignedData?.relations?.annotations != null) {
-//                        Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}")
-//                        handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
-//                         EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
-//                                 ?.let {
-//                                     TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
-//                                             ?.forEach { tet -> tet.annotations = it }
-//                                 }
-//                    }
                 }
                 EventType.REDACTION -> {
                     val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
@@ -217,9 +202,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                     when (eventToPrune.type) {
                         EventType.MESSAGE -> {
                             Timber.d("REDACTION for message ${eventToPrune.eventId}")
-//                                val unsignedData = EventMapper.map(eventToPrune).unsignedData
-//                                        ?: UnsignedData(null, null)
-
                             // was this event a m.replace
                             val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
                             if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
@@ -231,34 +213,34 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         }
                     }
                 }
-                in EventType.POLL_START -> {
+                in EventType.POLL_START.values -> {
                     val content: MessagePollContent? = event.content.toModel()
                     if (content?.relatesTo?.type == RelationType.REPLACE) {
                         Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
                         // A replace!
-                        handleReplace(realm, event, content, roomId, isLocalEcho)
+                        handleReplace(realm, event, roomId, isLocalEcho, content.relatesTo.eventId)
                     }
                 }
-                in EventType.POLL_RESPONSE -> {
+                in EventType.POLL_RESPONSE.values -> {
                     event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
                         sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
                             pollAggregationProcessor.handlePollResponseEvent(session, realm, event)
                         }
                     }
                 }
-                in EventType.POLL_END -> {
+                in EventType.POLL_END.values -> {
                     sessionManager.getSessionComponent(sessionId)?.session()?.let { session ->
                         getPowerLevelsHelper(event.roomId)?.let {
                             pollAggregationProcessor.handlePollEndEvent(session, it, realm, event)
                         }
                     }
                 }
-                in EventType.STATE_ROOM_BEACON_INFO -> {
+                in EventType.STATE_ROOM_BEACON_INFO.values -> {
                     event.content.toModel<MessageBeaconInfoContent>(catchError = true)?.let {
                         liveLocationAggregationProcessor.handleBeaconInfo(realm, event, it, roomId, isLocalEcho)
                     }
                 }
-                in EventType.BEACON_LOCATION_DATA -> {
+                in EventType.BEACON_LOCATION_DATA.values -> {
                     handleBeaconLocationData(event, realm, roomId, isLocalEcho)
                 }
                 else -> Timber.v("UnHandled event ${event.eventId}")
@@ -274,23 +256,22 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
     private fun handleReplace(
             realm: Realm,
             event: Event,
-            content: MessageContent,
             roomId: String,
             isLocalEcho: Boolean,
-            relatedEventId: String? = null
+            relatedEventId: String?
     ) {
         val eventId = event.eventId ?: return
-        val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
-        val newContent = content.newContent ?: return
-
-        // Check that the sender is the same
+        val targetEventId = relatedEventId ?: return
         val editedEvent = EventEntity.where(realm, targetEventId).findFirst()
-        if (editedEvent == null) {
-            // We do not know yet about the edited event
-        } else if (editedEvent.sender != event.senderId) {
-            // Edited by someone else, ignore
-            Timber.w("Ignore edition by someone else")
-            return
+
+        when (val validity = editValidator.validateEdit(editedEvent?.asDomain(), event)) {
+            is EventEditValidator.EditValidity.Invalid -> return Unit.also {
+                Timber.w("Dropping invalid edit ${event.eventId}, reason:${validity.reason}")
+            }
+            EventEditValidator.EditValidity.Unknown, // we can't drop the source event might be unknown, will be validated later
+            EventEditValidator.EditValidity.Valid -> {
+                // continue
+            }
         }
 
         // ok, this is a replace
@@ -305,11 +286,10 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                     .also { editSummary ->
                         editSummary.editions.add(
                                 EditionOfEvent(
-                                        senderId = event.senderId ?: "",
                                         eventId = event.eventId,
-                                        content = ContentMapper.map(newContent),
-                                        timestamp = if (isLocalEcho) 0 else event.originServerTs ?: 0,
-                                        isLocalEcho = isLocalEcho
+                                        event = EventEntity.where(realm, eventId).findFirst(),
+                                        timestamp = if (isLocalEcho) clock.epochMillis() else event.originServerTs ?: clock.epochMillis(),
+                                        isLocalEcho = isLocalEcho,
                                 )
                         )
                     }
@@ -326,17 +306,17 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                 // ok it has already been managed
                 Timber.v("###REPLACE Receiving remote echo of edit (edit already done)")
                 existingSummary.editions.firstOrNull { it.eventId == txId }?.let {
-                    it.eventId = event.eventId
+                    it.eventId = eventId
                     it.timestamp = event.originServerTs ?: clock.epochMillis()
                     it.isLocalEcho = false
+                    it.event = EventEntity.where(realm, eventId).findFirst()
                 }
             } else {
                 Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
                 existingSummary.editions.add(
                         EditionOfEvent(
-                                senderId = event.senderId ?: "",
-                                eventId = event.eventId,
-                                content = ContentMapper.map(newContent),
+                                eventId = eventId,
+                                event = EventEntity.where(realm, eventId).findFirst(),
                                 timestamp = if (isLocalEcho) {
                                     clock.epochMillis()
                                 } else {
@@ -349,7 +329,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             }
         }
 
-        if (event.getClearType() in EventType.POLL_START) {
+        if (event.getClearType() in EventType.POLL_START.values) {
             pollAggregationProcessor.handlePollStartEvent(realm, event)
         }
 
@@ -501,7 +481,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
         }
         val sourceToDiscard = eventSummary.editSummary?.editions?.firstOrNull { it.eventId == redacted.eventId }
         if (sourceToDiscard == null) {
-            Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard")
+            Timber.w("Redaction of a replace that was not known in aggregation")
             return
         }
         // Need to remove this event from the edition list
@@ -599,12 +579,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
     private fun handleBeaconLocationData(event: Event, realm: Realm, roomId: String, isLocalEcho: Boolean) {
         event.getClearContent().toModel<MessageBeaconLocationDataContent>(catchError = true)?.let {
             liveLocationAggregationProcessor.handleBeaconLocationData(
-                    realm,
-                    event,
-                    it,
-                    roomId,
-                    event.getRelationContent()?.eventId,
-                    isLocalEcho
+                    realm = realm,
+                    event = event,
+                    content = it,
+                    roomId = roomId,
+                    relatedEventId = event.getRelationContent()?.eventId,
+                    isLocalEcho = isLocalEcho
             )
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 9bcb7b8e4c7fba65c7171c1b16c95ca0bfaa6fac..31bed90b622a280c9954bdcb76d9b4abf9234ad7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.session.room.membership.RoomMembersRespon
 import org.matrix.android.sdk.internal.session.room.membership.admin.UserIdAndReason
 import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBody
 import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
+import org.matrix.android.sdk.internal.session.room.read.ReadBody
 import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
 import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody
 import org.matrix.android.sdk.internal.session.room.send.SendResponse
@@ -173,7 +174,7 @@ internal interface RoomAPI {
             @Path("roomId") roomId: String,
             @Path("receiptType") receiptType: String,
             @Path("eventId") eventId: String,
-            @Body body: JsonDict = emptyMap()
+            @Body body: ReadBody
     )
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
index 03c2b2a47e97072736054996868a5c5e543e02a4..0cda6eca996f761468a1b569b99323264ad2aa91 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -21,6 +21,7 @@ import io.realm.Realm
 import io.realm.RealmConfiguration
 import io.realm.kotlin.createObject
 import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.runBlocking
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
 import org.matrix.android.sdk.api.session.room.model.Membership
@@ -95,7 +96,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
      * Create a local room entity from the given room creation params.
      * This will also generate and store in database the chunk and the events related to the room params in order to retrieve and display the local room.
      */
-    private suspend fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
+    private fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
         RoomEntity.getOrCreate(realm, roomId).apply {
             membership = Membership.JOIN
             chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody))
@@ -148,13 +149,16 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
      *
      * @return a chunk entity
      */
-    private suspend fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity {
+    private fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity {
         val chunkEntity = realm.createObject<ChunkEntity>().apply {
             isLastBackward = true
             isLastForward = true
         }
 
-        val eventList = createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody))
+        // Can't suspend when using realm as it could jump thread
+        val eventList = runBlocking {
+            createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody))
+        }
         val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
 
         for (event in eventList) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt
index eb966b684c8b7d2fee293d132f2a140b9e489d59..8b5fde6ab7e53238b87fd76620be84c8fcb24f09 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/RoomCreateEventProcessor.kt
@@ -30,7 +30,7 @@ import javax.inject.Inject
 
 internal class RoomCreateEventProcessor @Inject constructor() : EventInsertLiveProcessor {
 
-    override suspend fun process(realm: Realm, event: Event) {
+    override fun process(realm: Realm, event: Event) {
         val createRoomContent = event.getClearContent().toModel<RoomCreateContent>()
         val predecessorRoomId = createRoomContent?.predecessor?.roomId ?: return
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
index 60312071d7010f7859e6bcb915202d306d65a0cd..c36efa064f06ea8821d81b28af99de50c74d5e04 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
@@ -73,7 +73,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
         return sendLiveLocationTask.execute(params)
     }
 
-    override suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult {
+    override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult {
         // Ensure to stop any active live before starting a new one
         if (checkIfExistingActiveLive()) {
             val result = stopLiveLocationShare()
@@ -84,7 +84,6 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
         val params = StartLiveLocationShareTask.Params(
                 roomId = roomId,
                 timeoutMillis = timeoutMillis,
-                description = description
         )
         return startLiveLocationShareTask.execute(params)
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
index a8d955af1dd0652641763d745f3b88996aa5f434..ae7022a204db13778caa82bbf627653cc1033833 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
@@ -39,7 +39,7 @@ internal class DefaultGetActiveBeaconInfoForUserTask @Inject constructor(
 ) : GetActiveBeaconInfoForUserTask {
 
     override suspend fun execute(params: GetActiveBeaconInfoForUserTask.Params): Event? {
-        return EventType.STATE_ROOM_BEACON_INFO
+        return EventType.STATE_ROOM_BEACON_INFO.values
                 .mapNotNull {
                     stateEventDataSource.getStateEvent(
                             roomId = params.roomId,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt
index fa3479ed3c3155d625a499ff55f8d89890e92cc8..dbdc5dc2289bb4b1dae9a1080bf35777e852ad33 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt
@@ -40,7 +40,7 @@ internal class LiveLocationShareRedactionEventProcessor @Inject constructor() :
         return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO
     }
 
-    override suspend fun process(realm: Realm, event: Event) {
+    override fun process(realm: Realm, event: Event) {
         if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) {
             return
         }
@@ -48,7 +48,7 @@ internal class LiveLocationShareRedactionEventProcessor @Inject constructor() :
         val redactedEvent = EventEntity.where(realm, eventId = event.redacts).findFirst()
                 ?: return
 
-        if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO) {
+        if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO.values) {
             val liveSummary = LiveLocationShareAggregatedSummaryEntity.get(realm, eventId = redactedEvent.eventId)
 
             if (liveSummary != null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
index 79019e47658123085b9abb3bcf632170251812a7..13753115ac556d00f976d6c786208f2b9e3d1cb4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
@@ -30,7 +30,6 @@ internal interface StartLiveLocationShareTask : Task<StartLiveLocationShareTask.
     data class Params(
             val roomId: String,
             val timeoutMillis: Long,
-            val description: String,
     )
 }
 
@@ -42,12 +41,12 @@ internal class DefaultStartLiveLocationShareTask @Inject constructor(
 
     override suspend fun execute(params: StartLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
         val beaconContent = MessageBeaconInfoContent(
-                body = params.description,
+                body = "Live location",
                 timeout = params.timeoutMillis,
                 isLive = true,
                 unstableTimestampMillis = clock.epochMillis()
         ).toContent()
-        val eventType = EventType.STATE_ROOM_BEACON_INFO.first()
+        val eventType = EventType.STATE_ROOM_BEACON_INFO.stable
         val sendStateTaskParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = userId,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
index da5fd76940dbffba32cfc02bc99bd519af62f06b..40f7aa2dd2a1829aeb8a0e4052fc4a3de6776b7f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
@@ -45,7 +45,7 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor(
         val sendStateTaskParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = stateKey,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
                 body = updatedContent
         )
         return try {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
index cc86679cbc79c8cbf2f0c5495420487a8f4dd0d7..0cff2c5a7cfbe1f58ff1c252b4b3e2e4ebf0a92c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
@@ -46,7 +46,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
         return eventType == EventType.REDACTION
     }
 
-    override suspend fun process(realm: Realm, event: Event) {
+    override fun process(realm: Realm, event: Event) {
         pruneEvent(realm, event)
     }
 
@@ -61,45 +61,36 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
         val isLocalEcho = LocalEcho.isLocalEchoId(redactionEvent.eventId ?: "")
         Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")
 
-        val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
-                ?: return
+        val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() ?: return
 
         val typeToPrune = eventToPrune.type
         val stateKey = eventToPrune.stateKey
         val allowedKeys = computeAllowedKeys(typeToPrune)
-        if (allowedKeys.isNotEmpty()) {
-            val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) }
-            eventToPrune.content = ContentMapper.map(prunedContent)
-        } else {
-            when (typeToPrune) {
-                EventType.ENCRYPTED,
-                EventType.MESSAGE,
-                in EventType.STATE_ROOM_BEACON_INFO,
-                in EventType.BEACON_LOCATION_DATA,
-                in EventType.POLL_START -> {
-                    Timber.d("REDACTION for message ${eventToPrune.eventId}")
-                    val unsignedData = EventMapper.map(eventToPrune).unsignedData
-                            ?: UnsignedData(null, null)
-
-                    // was this event a m.replace
+        when {
+            allowedKeys.isNotEmpty() -> {
+                val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) }
+                eventToPrune.content = ContentMapper.map(prunedContent)
+            }
+            canPruneEventType(typeToPrune) -> {
+                Timber.d("REDACTION for message ${eventToPrune.eventId}")
+                val unsignedData = EventMapper.map(eventToPrune).unsignedData ?: UnsignedData(null, null)
+
+                // was this event a m.replace
 //                    val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
 //                    if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
 //                        eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
 //                    }
 
-                    val modified = unsignedData.copy(redactedEvent = redactionEvent)
-                    // Deleting the content of a thread message will result to delete the thread relation, however threads are now dynamic
-                    // so there is not much of a problem
-                    eventToPrune.content = ContentMapper.map(emptyMap())
-                    eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
-                    eventToPrune.decryptionResultJson = null
-                    eventToPrune.decryptionErrorCode = null
-
-                    handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho)
-                }
-//                EventType.REACTION -> {
-//                    eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
-//                }
+                val modified = unsignedData.copy(redactedEvent = redactionEvent)
+                eventToPrune.content = ContentMapper.map(emptyMap())
+                eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
+                eventToPrune.decryptionResultJson = null
+                eventToPrune.decryptionErrorCode = null
+
+                handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho)
+            }
+            else -> {
+                Timber.w("Not pruning event (type $typeToPrune)")
             }
         }
         if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) {
@@ -167,4 +158,28 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
             else -> emptyList()
         }
     }
+
+    private fun canPruneEventType(eventType: String): Boolean {
+        return when {
+            EventType.isCallEvent(eventType) -> false
+            EventType.isVerificationEvent(eventType) -> false
+            eventType == EventType.STATE_ROOM_WIDGET_LEGACY ||
+                    eventType == EventType.STATE_ROOM_WIDGET ||
+                    eventType == EventType.STATE_ROOM_NAME ||
+                    eventType == EventType.STATE_ROOM_TOPIC ||
+                    eventType == EventType.STATE_ROOM_AVATAR ||
+                    eventType == EventType.STATE_ROOM_THIRD_PARTY_INVITE ||
+                    eventType == EventType.STATE_ROOM_GUEST_ACCESS ||
+                    eventType == EventType.STATE_SPACE_CHILD ||
+                    eventType == EventType.STATE_SPACE_PARENT ||
+                    eventType == EventType.STATE_ROOM_TOMBSTONE ||
+                    eventType == EventType.STATE_ROOM_HISTORY_VISIBILITY ||
+                    eventType == EventType.STATE_ROOM_RELATED_GROUPS ||
+                    eventType == EventType.STATE_ROOM_PINNED_EVENT ||
+                    eventType == EventType.STATE_ROOM_ENCRYPTION ||
+                    eventType == EventType.STATE_ROOM_SERVER_ACL ||
+                    eventType == EventType.REACTION -> false
+            else -> true
+        }
+    }
 }
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 b30c66c82ed9c3e956b08f316c0681bac7d77d30..36ec5e8dacd3adcc8d757278ce65e607380ceae0 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
@@ -30,17 +30,20 @@ import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
 import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
 import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
+import org.matrix.android.sdk.internal.database.query.forMainTimelineWhere
 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.session.homeserver.HomeServerCapabilitiesDataSource
 
 internal class DefaultReadService @AssistedInject constructor(
         @Assisted private val roomId: String,
         @SessionDatabase private val monarchy: Monarchy,
         private val setReadMarkersTask: SetReadMarkersTask,
         private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
-        @UserId private val userId: String
+        @UserId private val userId: String,
+        private val homeServerCapabilitiesDataSource: HomeServerCapabilitiesDataSource
 ) : ReadService {
 
     @AssistedFactory
@@ -48,17 +51,28 @@ internal class DefaultReadService @AssistedInject constructor(
         fun create(roomId: String): DefaultReadService
     }
 
-    override suspend fun markAsRead(params: ReadService.MarkAsReadParams) {
+    override suspend fun markAsRead(params: ReadService.MarkAsReadParams, mainTimeLineOnly: Boolean) {
+        val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) {
+            if (mainTimeLineOnly) ReadService.THREAD_ID_MAIN else null
+        } else {
+            null
+        }
         val taskParams = SetReadMarkersTask.Params(
                 roomId = roomId,
                 forceReadMarker = params.forceReadMarker(),
-                forceReadReceipt = params.forceReadReceipt()
+                forceReadReceipt = params.forceReadReceipt(),
+                readReceiptThreadId = readReceiptThreadId
         )
         setReadMarkersTask.execute(taskParams)
     }
 
-    override suspend fun setReadReceipt(eventId: String) {
-        val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId)
+    override suspend fun setReadReceipt(eventId: String, threadId: String) {
+        val readReceiptThreadId = if (homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true) {
+            threadId
+        } else {
+            null
+        }
+        val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId, readReceiptThreadId = readReceiptThreadId)
         setReadMarkersTask.execute(params)
     }
 
@@ -68,7 +82,8 @@ internal class DefaultReadService @AssistedInject constructor(
     }
 
     override fun isEventRead(eventId: String): Boolean {
-        return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId)
+        val shouldCheckIfReadInEventsThread = homeServerCapabilitiesDataSource.getHomeServerCapabilities()?.canUseThreadReadReceiptsAndNotifications == true
+        return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId, shouldCheckIfReadInEventsThread)
     }
 
     override fun getReadMarkerLive(): LiveData<Optional<String>> {
@@ -81,9 +96,9 @@ internal class DefaultReadService @AssistedInject constructor(
         }
     }
 
-    override fun getMyReadReceiptLive(): LiveData<Optional<String>> {
+    override fun getMyReadReceiptLive(threadId: String?): LiveData<Optional<String>> {
         val liveRealmData = monarchy.findAllMappedWithChanges(
-                { ReadReceiptEntity.where(it, roomId = roomId, userId = userId) },
+                { ReadReceiptEntity.where(it, roomId = roomId, userId = userId, threadId = threadId) },
                 { it.eventId }
         )
         return Transformations.map(liveRealmData) {
@@ -94,10 +109,11 @@ internal class DefaultReadService @AssistedInject constructor(
     override fun getUserReadReceipt(userId: String): String? {
         var eventId: String? = null
         monarchy.doWithRealm {
-            eventId = ReadReceiptEntity.where(it, roomId = roomId, userId = userId)
+            eventId = ReadReceiptEntity.forMainTimelineWhere(it, roomId = roomId, userId = userId)
                     .findFirst()
                     ?.eventId
         }
+
         return eventId
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9374de5d5fca1c8466decc7c77e2c951210d9674
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/ReadBody.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.session.room.read
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class ReadBody(
+        @Json(name = "thread_id") val threadId: String?,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
index a124a8a4c2509ffe6159717b6f14d552bc98c608..8e7592a8b4d4409cd901305138d96286bda7b10d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.read
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.events.model.LocalEcho
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.query.isEventRead
@@ -45,8 +46,9 @@ internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> {
             val roomId: String,
             val fullyReadEventId: String? = null,
             val readReceiptEventId: String? = null,
+            val readReceiptThreadId: String? = null,
             val forceReadReceipt: Boolean = false,
-            val forceReadMarker: Boolean = false
+            val forceReadMarker: Boolean = false,
     )
 }
 
@@ -61,12 +63,14 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
         @UserId private val userId: String,
         private val globalErrorReceiver: GlobalErrorReceiver,
         private val clock: Clock,
+        private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
 ) : SetReadMarkersTask {
 
     override suspend fun execute(params: SetReadMarkersTask.Params) {
         val markers = mutableMapOf<String, String>()
         Timber.v("Execute set read marker with params: $params")
         val latestSyncedEventId = latestSyncedEventId(params.roomId)
+        val readReceiptThreadId = params.readReceiptThreadId
         val fullyReadEventId = if (params.forceReadMarker) {
             latestSyncedEventId
         } else {
@@ -77,6 +81,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
         } else {
             params.readReceiptEventId
         }
+
         if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy.realmConfiguration, params.roomId, fullyReadEventId)) {
             if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
                 Timber.w("Can't set read marker for local event $fullyReadEventId")
@@ -84,8 +89,12 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
                 markers[READ_MARKER] = fullyReadEventId
             }
         }
+
+        val shouldCheckIfReadInEventsThread = readReceiptThreadId != null &&
+                homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications
+
         if (readReceiptEventId != null &&
-                !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId)) {
+                !isEventRead(monarchy.realmConfiguration, userId, params.roomId, readReceiptEventId, shouldCheckIfReadInEventsThread)) {
             if (LocalEcho.isLocalEchoId(readReceiptEventId)) {
                 Timber.w("Can't set read receipt for local event $readReceiptEventId")
             } else {
@@ -95,7 +104,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
 
         val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId
         if (markers.isNotEmpty() || shouldUpdateRoomSummary) {
-            updateDatabase(params.roomId, markers, shouldUpdateRoomSummary)
+            updateDatabase(params.roomId, readReceiptThreadId, markers, shouldUpdateRoomSummary)
         }
         if (markers.isNotEmpty()) {
             executeRequest(
@@ -104,7 +113,8 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
             ) {
                 if (markers[READ_MARKER] == null) {
                     if (readReceiptEventId != null) {
-                        roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId)
+                        val readBody = ReadBody(threadId = params.readReceiptThreadId)
+                        roomAPI.sendReceipt(params.roomId, READ_RECEIPT, readReceiptEventId, readBody)
                     }
                 } else {
                     // "m.fully_read" value is mandatory to make this call
@@ -119,7 +129,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
                 TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId
             }
 
-    private suspend fun updateDatabase(roomId: String, markers: Map<String, String>, shouldUpdateRoomSummary: Boolean) {
+    private suspend fun updateDatabase(roomId: String, readReceiptThreadId: String?, markers: Map<String, String>, shouldUpdateRoomSummary: Boolean) {
         monarchy.awaitTransaction { realm ->
             val readMarkerId = markers[READ_MARKER]
             val readReceiptId = markers[READ_RECEIPT]
@@ -127,7 +137,7 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
                 roomFullyReadHandler.handle(realm, roomId, FullyReadContent(readMarkerId))
             }
             if (readReceiptId != null) {
-                val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, clock.epochMillis())
+                val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId, readReceiptThreadId, clock.epochMillis())
                 readReceiptHandler.handle(realm, roomId, readReceiptContent, false, null)
             }
             if (shouldUpdateRoomSummary) {
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 55ba78c2a59dd5811d92b332c4d47b3a5ad07b3e..2f8be694732d81e17adc239a9e2d76037597a12c 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
@@ -181,7 +181,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START.first(),
+                type = EventType.POLL_START.stable,
                 content = newContent.toContent().plus(additionalContent.orEmpty())
         )
     }
@@ -206,7 +206,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_RESPONSE.first(),
+                type = EventType.POLL_RESPONSE.stable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
@@ -226,7 +226,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START.first(),
+                type = EventType.POLL_START.stable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
@@ -249,7 +249,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_END.first(),
+                type = EventType.POLL_END.stable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
@@ -300,7 +300,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.BEACON_LOCATION_DATA.first(),
+                type = EventType.BEACON_LOCATION_DATA.stable,
                 content = content.toContent().plus(additionalContent.orEmpty()),
                 unsignedData = UnsignedData(age = null, transactionId = localId)
         )
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 7c83a4afa79c2f81742e5a980eacaf061b3666bc..21a0862c652882937d45e31271b4c7cb3eb68b4b 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
@@ -24,6 +24,7 @@ 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.content.EncryptionEventContent
 import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
@@ -75,7 +76,8 @@ internal class RoomSummaryUpdater @Inject constructor(
         private val roomAvatarResolver: RoomAvatarResolver,
         private val eventDecryptor: EventDecryptor,
         private val crossSigningService: DefaultCrossSigningService,
-        private val roomAccountDataDataSource: RoomAccountDataDataSource
+        private val roomAccountDataDataSource: RoomAccountDataDataSource,
+        private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
 ) {
 
     fun refreshLatestPreviewContent(realm: Realm, roomId: String) {
@@ -151,9 +153,11 @@ internal class RoomSummaryUpdater @Inject constructor(
             latestPreviewableEvent.attemptToDecrypt()
         }
 
+        val shouldCheckIfReadInEventsThread = homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications
+
         roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 ||
                 // avoid this call if we are sure there are unread events
-                latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId) } ?: false
+                latestPreviewableEvent?.let { !isEventRead(realm.configuration, userId, roomId, it.eventId, shouldCheckIfReadInEventsThread) } ?: false
 
         roomSummaryEntity.setDisplayName(roomDisplayNameResolver.resolve(realm, roomId))
         roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId)
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 c380ccf14f86e91e2afdde90640563c426ff9356..0854cc5cf412b21cdb04e8a5f0d2bd023142595d 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
@@ -411,7 +411,7 @@ internal class DefaultTimeline(
     private fun ensureReadReceiptAreLoaded(realm: Realm) {
         readReceiptHandler.getContentFromInitSync(roomId)
                 ?.also {
-                    Timber.w("INIT_SYNC Insert when opening timeline RR for room $roomId")
+                    Timber.d("INIT_SYNC Insert when opening timeline RR for room $roomId")
                 }
                 ?.let { readReceiptContent ->
                     realm.executeTransactionAsync {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt
index 96646b42edbf6bafd6595894b1c2becc2d484373..9d8d8ecbf1994f376a7fd9f96280ecc97589592c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/FetchTokenAndPaginateTask.kt
@@ -47,7 +47,7 @@ internal class DefaultFetchTokenAndPaginateTask @Inject constructor(
 ) : FetchTokenAndPaginateTask {
 
     override suspend fun execute(params: FetchTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result {
-        val filter = filterRepository.getRoomFilter()
+        val filter = filterRepository.getRoomFilterBody()
         val response = executeRequest(globalErrorReceiver) {
             roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter)
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt
index 015e55f070365d45453d392ddbf4dc19c6fb6e41..c3911dfa2c52ceb34999c80ba18506b2d5260ca7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetContextOfEventTask.kt
@@ -39,7 +39,7 @@ internal class DefaultGetContextOfEventTask @Inject constructor(
 ) : GetContextOfEventTask {
 
     override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
-        val filter = filterRepository.getRoomFilter()
+        val filter = filterRepository.getRoomFilterBody()
         val response = executeRequest(globalErrorReceiver) {
             // We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process.
             roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt
index 8aeccb66c898ccb2fe777d702fd5c4b1370e7859..1a7b1cdac482db134b1c023f71b488d8efde4093 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/PaginationTask.kt
@@ -41,7 +41,7 @@ internal class DefaultPaginationTask @Inject constructor(
 ) : PaginationTask {
 
     override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result {
-        val filter = filterRepository.getRoomFilter()
+        val filter = filterRepository.getRoomFilterBody()
         val chunk = executeRequest(
                 globalErrorReceiver,
                 canRetry = true
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt
index 2b404775f0bc882baea56a9bc33b887ab1e3a480..3684bec16756a99600585608e5d6211f4ee041d3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/tombstone/RoomTombstoneEventProcessor.kt
@@ -30,7 +30,7 @@ import javax.inject.Inject
 
 internal class RoomTombstoneEventProcessor @Inject constructor() : EventInsertLiveProcessor {
 
-    override suspend fun process(realm: Realm, event: Event) {
+    override fun process(realm: Realm, event: Event) {
         if (event.roomId == null) return
         val createRoomContent = event.getClearContent().toModel<RoomTombstoneContent>()
         if (createRoomContent?.replacementRoomId == null) return
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
index bc1a69769d6ce3b2d3e579fa450883217088d7cf..8a287fb0b4ec2e47e11079d5c54f2873f96b72f4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
@@ -36,7 +36,7 @@ import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.network.toFailure
 import org.matrix.android.sdk.internal.session.SessionListeners
 import org.matrix.android.sdk.internal.session.dispatchTo
-import org.matrix.android.sdk.internal.session.filter.FilterRepository
+import org.matrix.android.sdk.internal.session.filter.GetCurrentFilterTask
 import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask
 import org.matrix.android.sdk.internal.session.sync.parsing.InitialSyncResponseParser
 import org.matrix.android.sdk.internal.session.user.UserStore
@@ -64,11 +64,9 @@ internal interface SyncTask : Task<SyncTask.Params, SyncResponse> {
 internal class DefaultSyncTask @Inject constructor(
         private val syncAPI: SyncAPI,
         @UserId private val userId: String,
-        private val filterRepository: FilterRepository,
         private val syncResponseHandler: SyncResponseHandler,
         private val syncRequestStateTracker: SyncRequestStateTracker,
         private val syncTokenStore: SyncTokenStore,
-        private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
         private val userStore: UserStore,
         private val session: Session,
         private val sessionListeners: SessionListeners,
@@ -79,6 +77,8 @@ internal class DefaultSyncTask @Inject constructor(
         private val syncResponseParser: InitialSyncResponseParser,
         private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore,
         private val clock: Clock,
+        private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
+        private val getCurrentFilterTask: GetCurrentFilterTask
 ) : SyncTask {
 
     private val workingDir = File(fileDirectory, "is")
@@ -100,8 +100,13 @@ internal class DefaultSyncTask @Inject constructor(
             requestParams["since"] = token
             timeout = params.timeout
         }
+
+        // Maybe refresh the homeserver capabilities data we know
+        getHomeServerCapabilitiesTask.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false))
+        val filter = getCurrentFilterTask.execute(Unit)
+
         requestParams["timeout"] = timeout.toString()
-        requestParams["filter"] = filterRepository.getFilter()
+        requestParams["filter"] = filter
         params.presence?.let { requestParams["set_presence"] = it.value }
 
         val isInitialSync = token == null
@@ -115,8 +120,6 @@ internal class DefaultSyncTask @Inject constructor(
             )
             syncRequestStateTracker.startRoot(InitialSyncStep.ImportingAccount, 100)
         }
-        // Maybe refresh the homeserver capabilities data we know
-        getHomeServerCapabilitiesTask.execute(GetHomeServerCapabilitiesTask.Params(forceRefresh = false))
 
         val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
index 7329611a01776e50577334842457a40bff73ded7..7f12ce653c7299aef478a367fcea076cf2ff245f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
@@ -33,10 +33,11 @@ import javax.inject.Inject
 // value : dict key $UserId
 //              value dict key ts
 //                    dict value ts value
-internal typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Double>>>>
+internal typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Any>>>>
 
 private const val READ_KEY = "m.read"
 private const val TIMESTAMP_KEY = "ts"
+private const val THREAD_ID_KEY = "thread_id"
 
 internal class ReadReceiptHandler @Inject constructor(
         private val roomSyncEphemeralTemporaryStore: RoomSyncEphemeralTemporaryStore
@@ -47,14 +48,19 @@ internal class ReadReceiptHandler @Inject constructor(
         fun createContent(
                 userId: String,
                 eventId: String,
+                threadId: String?,
                 currentTimeMillis: Long
         ): ReadReceiptContent {
+            val userReadReceipt = mutableMapOf<String, Any>(
+                    TIMESTAMP_KEY to currentTimeMillis.toDouble(),
+            )
+            threadId?.let {
+                userReadReceipt.put(THREAD_ID_KEY, threadId)
+            }
             return mapOf(
                     eventId to mapOf(
                             READ_KEY to mapOf(
-                                    userId to mapOf(
-                                            TIMESTAMP_KEY to currentTimeMillis.toDouble()
-                                    )
+                                    userId to userReadReceipt
                             )
                     )
             )
@@ -98,8 +104,9 @@ internal class ReadReceiptHandler @Inject constructor(
             val readReceiptsSummary = ReadReceiptsSummaryEntity(eventId = eventId, roomId = roomId)
 
             for ((userId, paramsDict) in userIdsDict) {
-                val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0
-                val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, ts)
+                val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0
+                val threadId = paramsDict[THREAD_ID_KEY] as String?
+                val receiptEntity = ReadReceiptEntity.createUnmanaged(roomId, eventId, userId, threadId, ts)
                 readReceiptsSummary.readReceipts.add(receiptEntity)
             }
             readReceiptSummaries.add(readReceiptsSummary)
@@ -115,7 +122,7 @@ internal class ReadReceiptHandler @Inject constructor(
     ) {
         // First check if we have data from init sync to handle
         getContentFromInitSync(roomId)?.let {
-            Timber.w("INIT_SYNC Insert during incremental sync RR for room $roomId")
+            Timber.d("INIT_SYNC Insert during incremental sync RR for room $roomId")
             doIncrementalSyncStrategy(realm, roomId, it)
             aggregator?.ephemeralFilesToDelete?.add(roomId)
         }
@@ -132,8 +139,9 @@ internal class ReadReceiptHandler @Inject constructor(
                     }
 
             for ((userId, paramsDict) in userIdsDict) {
-                val ts = paramsDict[TIMESTAMP_KEY] ?: 0.0
-                val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId)
+                val ts = paramsDict[TIMESTAMP_KEY] as? Double ?: 0.0
+                val threadId = paramsDict[THREAD_ID_KEY] as String?
+                val receiptEntity = ReadReceiptEntity.getOrCreate(realm, roomId, userId, threadId)
                 // ensure new ts is superior to the previous one
                 if (ts > receiptEntity.originServerTs) {
                     ReadReceiptsSummaryEntity.where(realm, receiptEntity.eventId).findFirst()?.also {
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 2825be8291d48491b79c3f1bedb8fb5f9f06277f..4001ae2ccfc0489013269c4a5baae887e5b12308 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
@@ -25,6 +25,8 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import org.matrix.android.sdk.api.session.events.model.getRelationContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.api.session.room.model.Membership
@@ -49,6 +51,7 @@ import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.mapper.toEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.database.model.RoomEntity
@@ -486,23 +489,41 @@ internal class RoomSyncHandler @Inject constructor(
             cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync)
 
             // Try to remove local echo
-            event.unsignedData?.transactionId?.also {
-                val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
+            event.unsignedData?.transactionId?.also { txId ->
+                val sendingEventEntity = roomEntity.sendingTimelineEvents.find(txId)
                 if (sendingEventEntity != null) {
-                    Timber.v("Remove local echo for tx:$it")
+                    Timber.v("Remove local echo for tx:$txId")
                     roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
                     if (event.isEncrypted() && event.content?.get("algorithm") as? String == MXCRYPTO_ALGORITHM_MEGOLM) {
-                        // updated with echo decryption, to avoid seeing it decrypt again
+                        // updated with echo decryption, to avoid seeing txId decrypt again
                         val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
                         sendingEventEntity.root?.decryptionResultJson?.let { json ->
                             eventEntity.decryptionResultJson = json
                             event.mxDecryptionResult = adapter.fromJson(json)
                         }
                     }
+                    // also update potential edit that could refer to that event?
+                    // If not display will flicker :/
+                    val relationContent = event.getRelationContent()
+                    if (relationContent?.type == RelationType.REPLACE) {
+                        relationContent.eventId?.let { targetId ->
+                            EventAnnotationsSummaryEntity.where(realm, roomId, targetId)
+                                    .findFirst()
+                                    ?.editSummary
+                                    ?.editions
+                                    ?.forEach {
+                                        if (it.eventId == txId) {
+                                            // just do that, the aggregation processor will to the rest
+                                            it.event = eventEntity
+                                        }
+                                    }
+                        }
+                    }
+
                     // Finally delete the local echo
                     sendingEventEntity.deleteOnCascade(true)
                 } else {
-                    Timber.v("Can't find corresponding local echo for tx:$it")
+                    Timber.v("Can't find corresponding local echo for tx:$txId")
                 }
             }
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a7de7f55796c008b9234ce79cac4559f300c6776
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/sync/filter/SyncFilterParams.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.sync.filter
+
+internal data class SyncFilterParams(
+        val lazyLoadMembersForStateEvents: Boolean? = null,
+        val lazyLoadMembersForMessageEvents: Boolean? = null,
+        val useThreadNotifications: Boolean? = null,
+        val listOfSupportedEventTypes: List<String>? = null,
+        val listOfSupportedStateEventTypes: List<String>? = null,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt
index 6152eacae55b7ad723835e7b1e12b188725638c4..af3ba80fe4fe1ef4f25275e9a207f4f28f48d3f6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt
@@ -22,7 +22,7 @@ import io.realm.RealmModel
 import org.matrix.android.sdk.internal.database.awaitTransaction
 import java.util.concurrent.atomic.AtomicReference
 
-internal suspend fun <T> Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T {
+internal suspend fun <T> Monarchy.awaitTransaction(transaction: (realm: Realm) -> T): T {
     return awaitTransaction(realmConfiguration, transaction)
 }
 
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7ad5bb40e3f52eb4026b59af54ccf6d78c696d4c
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/EditAggregatedSummaryEntityMapperTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.mapper
+
+import io.mockk.every
+import io.mockk.mockk
+import io.realm.RealmList
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldNotBe
+import org.junit.Test
+import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.EditionOfEvent
+import org.matrix.android.sdk.internal.database.model.EventEntity
+
+class EditAggregatedSummaryEntityMapperTest {
+
+    @Test
+    fun `test mapping summary entity to model`() {
+        val edits = RealmList<EditionOfEvent>(
+                EditionOfEvent(
+                        timestamp = 0L,
+                        eventId = "e0",
+                        isLocalEcho = false,
+                        event = mockEvent("e0")
+                ),
+                EditionOfEvent(
+                        timestamp = 1L,
+                        eventId = "e1",
+                        isLocalEcho = false,
+                        event = mockEvent("e1")
+                ),
+                EditionOfEvent(
+                        timestamp = 30L,
+                        eventId = "e2",
+                        isLocalEcho = true,
+                        event = mockEvent("e2")
+                )
+        )
+        val fakeSummaryEntity = mockk<EditAggregatedSummaryEntity> {
+            every { editions } returns edits
+        }
+
+        val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity)
+        mapped shouldNotBe null
+        mapped!!.sourceEvents.size shouldBeEqualTo 2
+        mapped.localEchos.size shouldBeEqualTo 1
+        mapped.localEchos.first() shouldBeEqualTo "e2"
+
+        mapped.lastEditTs shouldBeEqualTo 30L
+        mapped.latestEdit?.eventId shouldBeEqualTo "e2"
+    }
+
+    @Test
+    fun `event with lexicographically largest event_id is treated as more recent`() {
+        val lowerId = "\$Albatross"
+        val higherId = "\$Zebra"
+
+        (higherId > lowerId) shouldBeEqualTo true
+        val timestamp = 1669288766745L
+        val edits = RealmList<EditionOfEvent>(
+                EditionOfEvent(
+                        timestamp = timestamp,
+                        eventId = lowerId,
+                        isLocalEcho = false,
+                        event = mockEvent(lowerId)
+                ),
+                EditionOfEvent(
+                        timestamp = timestamp,
+                        eventId = higherId,
+                        isLocalEcho = false,
+                        event = mockEvent(higherId)
+                ),
+                EditionOfEvent(
+                        timestamp = 1L,
+                        eventId = "e2",
+                        isLocalEcho = true,
+                        event = mockEvent("e2")
+                )
+        )
+
+        val fakeSummaryEntity = mockk<EditAggregatedSummaryEntity> {
+            every { editions } returns edits
+        }
+        val mapped = EditAggregatedSummaryEntityMapper.map(fakeSummaryEntity)
+        mapped!!.lastEditTs shouldBeEqualTo timestamp
+        mapped.latestEdit?.eventId shouldBeEqualTo higherId
+    }
+
+    private fun mockEvent(eventId: String): EventEntity {
+        return EventEntity().apply {
+            this.eventId = eventId
+            this.content = """
+                {
+                    "body" : "Hello",
+                    "msgtype": "text"
+                }
+            """.trimIndent()
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5fda242b90fb3408845ce58d339452937eca33d1
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/event/ValidDecryptedEventTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.event
+
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldNotBe
+import org.junit.Test
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.events.model.toValidDecryptedEvent
+import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
+
+class ValidDecryptedEventTest {
+
+    private val fakeEvent = Event(
+            type = EventType.ENCRYPTED,
+            eventId = "\$eventId",
+            roomId = "!fakeRoom",
+            content = EncryptedEventContent(
+                    algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
+                    ciphertext = "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...",
+                    sessionId = "TO2G4u2HlnhtbIJk",
+                    senderKey = "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0",
+                    deviceId = "FAKEE"
+            ).toContent()
+    )
+
+    @Test
+    fun `A failed to decrypt message should give a null validated decrypted event`() {
+        fakeEvent.toValidDecryptedEvent() shouldBe null
+    }
+
+    @Test
+    fun `Mismatch sender key detection`() {
+        val decryptedEvent = fakeEvent
+                .apply {
+                    mxDecryptionResult = OlmDecryptionResult(
+                            payload = mapOf(
+                                    "type" to EventType.MESSAGE,
+                                    "content" to mapOf(
+                                            "body" to "some message",
+                                            "msgtype" to "m.text"
+                                    ),
+                            ),
+                            senderKey = "the_real_sender_key",
+                    )
+                }
+
+        val validDecryptedEvent = decryptedEvent.toValidDecryptedEvent()
+        validDecryptedEvent shouldNotBe null
+
+        fakeEvent.content!!["senderKey"] shouldNotBe "the_real_sender_key"
+        validDecryptedEvent!!.cryptoSenderKey shouldBe "the_real_sender_key"
+    }
+
+    @Test
+    fun `Mixed content event should be detected`() {
+        val mixedEvent = Event(
+                type = EventType.ENCRYPTED,
+                eventId = "\$eventd ",
+                roomId = "!fakeRoo",
+                content = mapOf(
+                        "algorithm" to "m.megolm.v1.aes-sha2",
+                        "ciphertext" to "AwgBEpACQEKOkd4Gp0+gSXG4M+btcrnPgsF23xs/lUmS2I4YjmqF...",
+                        "sessionId" to "TO2G4u2HlnhtbIJk",
+                        "senderKey" to "5e3EIqg3JfooZnLQ2qHIcBarbassQ4qXblai0",
+                        "deviceId" to "FAKEE",
+                        "body" to "some message",
+                        "msgtype" to "m.text"
+                ).toContent()
+        )
+
+        val unValidatedContent = mixedEvent.getClearContent().toModel<MessageTextContent>()
+        unValidatedContent?.body shouldBe "some message"
+
+        mixedEvent.toValidDecryptedEvent()?.clearContent?.toModel<MessageTextContent>() shouldBe null
+    }
+
+    @Test
+    fun `Basic field validation`() {
+        val decryptedEvent = fakeEvent
+                .apply {
+                    mxDecryptionResult = OlmDecryptionResult(
+                            payload = mapOf(
+                                    "type" to EventType.MESSAGE,
+                                    "content" to mapOf(
+                                            "body" to "some message",
+                                            "msgtype" to "m.text"
+                                    ),
+                            ),
+                            senderKey = "the_real_sender_key",
+                    )
+                }
+
+        decryptedEvent.toValidDecryptedEvent() shouldNotBe null
+        decryptedEvent.copy(roomId = null).toValidDecryptedEvent() shouldBe null
+        decryptedEvent.copy(eventId = null).toValidDecryptedEvent() shouldBe null
+    }
+
+    @Test
+    fun `A clear event is not a valid decrypted event`() {
+        val mockTextEvent = Event(
+                type = EventType.MESSAGE,
+                eventId = "eventId",
+                roomId = "!fooe:example.com",
+                content = mapOf(
+                        "body" to "some message",
+                        "msgtype" to "m.text"
+                ),
+                originServerTs = 1000,
+                senderId = "@anne:example.com",
+        )
+        mockTextEvent.toValidDecryptedEvent() shouldBe null
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0ae712bff1b0211db93e5c10d252879ef48682dc
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventEditValidatorTest.kt
@@ -0,0 +1,372 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room
+
+import io.mockk.every
+import io.mockk.mockk
+import org.amshove.kluent.shouldBeInstanceOf
+import org.junit.Test
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+
+class EventEditValidatorTest {
+
+    private val mockTextEvent = Event(
+            type = EventType.MESSAGE,
+            eventId = "\$WX8WlNC2reiXrwHIA_CQHmU_pSR-jhOA2xKPRcJN9wQ",
+            roomId = "!GXKhWsRwiWWvbQDBpe:example.com",
+            content = mapOf(
+                    "body" to "some message",
+                    "msgtype" to "m.text"
+            ),
+            originServerTs = 1000,
+            senderId = "@alice:example.com",
+    )
+
+    private val mockEdit = Event(
+            type = EventType.MESSAGE,
+            eventId = "\$-SF7RWLPzRzCbHqK3ZAhIrX5Auh3B2lS5AqJiypt1p0",
+            roomId = "!GXKhWsRwiWWvbQDBpe:example.com",
+            content = mapOf(
+                    "body" to "* some message edited",
+                    "msgtype" to "m.text",
+                    "m.new_content" to mapOf(
+                            "body" to "some message edited",
+                            "msgtype" to "m.text"
+                    ),
+                    "m.relates_to" to mapOf(
+                            "rel_type" to "m.replace",
+                            "event_id" to mockTextEvent.eventId
+                    )
+            ),
+            originServerTs = 2000,
+            senderId = "@alice:example.com",
+    )
+
+    @Test
+    fun `edit should be valid`() {
+        val mockCryptoStore = mockk<IMXCryptoStore>()
+        val validator = EventEditValidator(mockCryptoStore)
+
+        validator
+                .validateEdit(mockTextEvent, mockEdit) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class
+    }
+
+    @Test
+    fun `original event and replacement event must have the same sender`() {
+        val mockCryptoStore = mockk<IMXCryptoStore>()
+        val validator = EventEditValidator(mockCryptoStore)
+
+        validator
+                .validateEdit(
+                        mockTextEvent,
+                        mockEdit.copy(senderId = "@bob:example.com")
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+    }
+
+    @Test
+    fun `original event and replacement event must have the same room_id`() {
+        val mockCryptoStore = mockk<IMXCryptoStore>()
+        val validator = EventEditValidator(mockCryptoStore)
+
+        validator
+                .validateEdit(
+                        mockTextEvent,
+                        mockEdit.copy(roomId = "!someotherroom")
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+
+        validator
+                .validateEdit(
+                        encryptedEvent,
+                        encryptedEditEvent.copy(roomId = "!someotherroom")
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+    }
+
+    @Test
+    fun `replacement and original events must not have a state_key property`() {
+        val mockCryptoStore = mockk<IMXCryptoStore>()
+        val validator = EventEditValidator(mockCryptoStore)
+
+        validator
+                .validateEdit(
+                        mockTextEvent,
+                        mockEdit.copy(stateKey = "")
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+
+        validator
+                .validateEdit(
+                        mockTextEvent.copy(stateKey = ""),
+                        mockEdit
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+    }
+
+    @Test
+    fun `replacement event must have an new_content property`() {
+        val mockCryptoStore = mockk<IMXCryptoStore> {
+            every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
+                    mockk<CryptoDeviceInfo> {
+                        every { userId } returns "@alice:example.com"
+                    }
+        }
+        val validator = EventEditValidator(mockCryptoStore)
+
+        validator
+                .validateEdit(mockTextEvent, mockEdit.copy(
+                        content = mockEdit.content!!.toMutableMap().apply {
+                            this.remove("m.new_content")
+                        }
+                )) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+
+        validator
+                .validateEdit(
+                        encryptedEvent,
+                        encryptedEditEvent.copy().apply {
+                            mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
+                                    payload = mapOf(
+                                            "type" to EventType.MESSAGE,
+                                            "content" to mapOf(
+                                                    "body" to "* some message edited",
+                                                    "msgtype" to "m.text",
+                                                    "m.relates_to" to mapOf(
+                                                            "rel_type" to "m.replace",
+                                                            "event_id" to mockTextEvent.eventId
+                                                    )
+                                            )
+                                    )
+                            )
+                        }
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+    }
+
+    @Test
+    fun `The original event must not itself have a rel_type of m_replace`() {
+        val mockCryptoStore = mockk<IMXCryptoStore> {
+            every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
+                    mockk<CryptoDeviceInfo> {
+                        every { userId } returns "@alice:example.com"
+                    }
+        }
+        val validator = EventEditValidator(mockCryptoStore)
+
+        validator
+                .validateEdit(
+                        mockTextEvent.copy(
+                                content = mockTextEvent.content!!.toMutableMap().apply {
+                                    this["m.relates_to"] = mapOf(
+                                            "rel_type" to "m.replace",
+                                            "event_id" to mockTextEvent.eventId
+                                    )
+                                }
+                        ),
+                        mockEdit
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+
+        validator
+                .validateEdit(
+                        encryptedEvent.copy(
+                                content = encryptedEvent.content!!.toMutableMap().apply {
+                                    put(
+                                            "m.relates_to",
+                                            mapOf(
+                                                    "rel_type" to "m.replace",
+                                                    "event_id" to mockTextEvent.eventId
+                                            )
+                                    )
+                                }
+                        ).apply {
+                            mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
+                                    payload = mapOf(
+                                            "type" to EventType.MESSAGE,
+                                            "content" to mapOf(
+                                                    "body" to "some message",
+                                                    "msgtype" to "m.text",
+                                            ),
+                                    )
+                            )
+                        },
+                        encryptedEditEvent
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+    }
+
+    @Test
+    fun `valid e2ee edit`() {
+        val mockCryptoStore = mockk<IMXCryptoStore> {
+            every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
+                    mockk<CryptoDeviceInfo> {
+                        every { userId } returns "@alice:example.com"
+                    }
+        }
+        val validator = EventEditValidator(mockCryptoStore)
+
+        validator
+                .validateEdit(
+                        encryptedEvent,
+                        encryptedEditEvent
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class
+    }
+
+    @Test
+    fun `If the original event was encrypted, the replacement should be too`() {
+        val mockCryptoStore = mockk<IMXCryptoStore> {
+            every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
+                    mockk<CryptoDeviceInfo> {
+                        every { userId } returns "@alice:example.com"
+                    }
+        }
+        val validator = EventEditValidator(mockCryptoStore)
+
+        validator
+                .validateEdit(
+                        encryptedEvent,
+                        mockEdit
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+    }
+
+    @Test
+    fun `encrypted, original event and replacement event must have the same sender`() {
+        val mockCryptoStore = mockk<IMXCryptoStore> {
+            every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
+                    mockk {
+                        every { userId } returns "@alice:example.com"
+                    }
+            every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns
+                    mockk {
+                        every { userId } returns "@bob:example.com"
+                    }
+        }
+        val validator = EventEditValidator(mockCryptoStore)
+
+        validator
+                .validateEdit(
+                        encryptedEvent,
+                        encryptedEditEvent.copy().apply {
+                            mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
+                                    senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI"
+                            )
+                        }
+
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+
+        // if sent fom a deleted device it should use the event claimed sender id
+    }
+
+    @Test
+    fun `encrypted, sent fom a deleted device, original event and replacement event must have the same sender`() {
+        val mockCryptoStore = mockk<IMXCryptoStore> {
+            every { deviceWithIdentityKey("R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo") } returns
+                    mockk {
+                        every { userId } returns "@alice:example.com"
+                    }
+            every { deviceWithIdentityKey("7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI") } returns
+                    null
+        }
+        val validator = EventEditValidator(mockCryptoStore)
+
+        validator
+                .validateEdit(
+                        encryptedEvent,
+                        encryptedEditEvent.copy().apply {
+                            mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
+                                    senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI"
+                            )
+                        }
+
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Valid::class
+
+        validator
+                .validateEdit(
+                        encryptedEvent,
+                        encryptedEditEvent.copy(
+                                senderId = "bob@example.com"
+                        ).apply {
+                            mxDecryptionResult = encryptedEditEvent.mxDecryptionResult!!.copy(
+                                    senderKey = "7V5e/2O93mf4GeW7Mtq4YWcRNpYS9NhQbdJMgdnIPUI"
+                            )
+                        }
+
+                ) shouldBeInstanceOf EventEditValidator.EditValidity.Invalid::class
+    }
+
+    private val encryptedEditEvent = Event(
+            type = EventType.ENCRYPTED,
+            eventId = "\$-SF7RWLPzRzCbHqK3ZAhIrX5Auh3B2lS5AqJiypt1p0",
+            roomId = "!GXKhWsRwiWWvbQDBpe:example.com",
+            content = mapOf(
+                    "algorithm" to "m.megolm.v1.aes-sha2",
+                    "sender_key" to "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo",
+                    "session_id" to "7tOd6xon/R2zJpy2LlSKcKWIek2jvkim0sNdnZZCWMQ",
+                    "device_id" to "QDHBLWOTSN",
+                    "ciphertext" to "AwgXErAC6TgQ4bV6NFldlffTWuUV1gsBYH6JLQMqG...deLfCQOSPunSSNDFdWuDkB8Cg",
+                    "m.relates_to" to mapOf(
+                            "rel_type" to "m.replace",
+                            "event_id" to mockTextEvent.eventId
+                    )
+            ),
+            originServerTs = 2000,
+            senderId = "@alice:example.com",
+    ).apply {
+        mxDecryptionResult = OlmDecryptionResult(
+                payload = mapOf(
+                        "type" to EventType.MESSAGE,
+                        "content" to mapOf(
+                                "body" to "* some message edited",
+                                "msgtype" to "m.text",
+                                "m.new_content" to mapOf(
+                                        "body" to "some message edited",
+                                        "msgtype" to "m.text"
+                                ),
+                                "m.relates_to" to mapOf(
+                                        "rel_type" to "m.replace",
+                                        "event_id" to mockTextEvent.eventId
+                                )
+                        )
+                ),
+                senderKey = "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo",
+                isSafe = true
+        )
+    }
+
+    private val encryptedEvent = Event(
+            type = EventType.ENCRYPTED,
+            eventId = "\$WX8WlNC2reiXrwHIA_CQHmU_pSR-jhOA2xKPRcJN9wQ",
+            roomId = "!GXKhWsRwiWWvbQDBpe:example.com",
+            content = mapOf(
+                    "algorithm" to "m.megolm.v1.aes-sha2",
+                    "sender_key" to "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo",
+                    "session_id" to "7tOd6xon/R2zJpy2LlSKcKWIek2jvkim0sNdnZZCWMQ",
+                    "device_id" to "QDHBLWOTSN",
+                    "ciphertext" to "AwgXErAC6TgQ4bV6NFldlffTWuUV1gsBYH6JLQMqG+4Vr...Yf0gYyhVWZY4SedF3fTMwkjmTuel4fwrmq",
+            ),
+            originServerTs = 2000,
+            senderId = "@alice:example.com",
+    ).apply {
+        mxDecryptionResult = OlmDecryptionResult(
+                payload = mapOf(
+                        "type" to EventType.MESSAGE,
+                        "content" to mapOf(
+                                "body" to "some message",
+                                "msgtype" to "m.text"
+                        ),
+                ),
+                senderKey = "R0s/7Aindgg/RNWqUGJyJOXtCz5H7Gx7fInFuroq1xo",
+                isSafe = true
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt
index 129d49633e2f9135500052efdba4093e0394236f..bdd1fd9b0d259937ca7874aad26a27956ca8cc6d 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollEventsTestData.kt
@@ -87,7 +87,7 @@ object PollEventsTestData {
     )
 
     internal val A_POLL_START_EVENT = Event(
-            type = EventType.POLL_START.first(),
+            type = EventType.POLL_START.stable,
             eventId = AN_EVENT_ID,
             originServerTs = 1652435922563,
             senderId = A_USER_ID_1,
@@ -96,7 +96,7 @@ object PollEventsTestData {
     )
 
     internal val A_POLL_RESPONSE_EVENT = Event(
-            type = EventType.POLL_RESPONSE.first(),
+            type = EventType.POLL_RESPONSE.stable,
             eventId = AN_EVENT_ID,
             originServerTs = 1652435922563,
             senderId = A_USER_ID_1,
@@ -105,7 +105,7 @@ object PollEventsTestData {
     )
 
     internal val A_POLL_END_EVENT = Event(
-            type = EventType.POLL_END.first(),
+            type = EventType.POLL_END.stable,
             eventId = AN_EVENT_ID,
             originServerTs = 1652435922563,
             senderId = A_USER_ID_1,
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
index d51ed77399825a85b144aba4d2796d51dc426b53..4a10795647c3bd34478e75c5f42508f3c74795de 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
@@ -69,7 +69,7 @@ class DefaultGetActiveBeaconInfoForUserTaskTest {
         result shouldBeEqualTo currentStateEvent
         fakeStateEventDataSource.verifyGetStateEvent(
                 roomId = params.roomId,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
                 stateKey = QueryStringValue.Equals(A_USER_ID)
         )
     }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt
index a01f51604ca6600a562a7135d187f40c9ef69b1b..1f15a9bee88736f2484d30e7a01bb4883a6bdd64 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt
@@ -53,7 +53,6 @@ private const val A_LATITUDE = 1.4
 private const val A_LONGITUDE = 40.0
 private const val AN_UNCERTAINTY = 5.0
 private const val A_TIMEOUT = 15_000L
-private const val A_DESCRIPTION = "description"
 private const val A_REASON = "reason"
 
 @ExperimentalCoroutinesApi
@@ -143,7 +142,7 @@ internal class DefaultLocationSharingServiceTest {
         coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id")
         coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
 
-        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION)
+        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
 
         result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
         val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
@@ -157,7 +156,6 @@ internal class DefaultLocationSharingServiceTest {
         val expectedStartParams = StartLiveLocationShareTask.Params(
                 roomId = A_ROOM_ID,
                 timeoutMillis = A_TIMEOUT,
-                description = A_DESCRIPTION
         )
         coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
     }
@@ -168,7 +166,7 @@ internal class DefaultLocationSharingServiceTest {
         val error = Throwable()
         coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error)
 
-        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION)
+        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
 
         result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
         val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
@@ -186,7 +184,7 @@ internal class DefaultLocationSharingServiceTest {
         coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false
         coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
 
-        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION)
+        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
 
         result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
         val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
@@ -196,7 +194,6 @@ internal class DefaultLocationSharingServiceTest {
         val expectedStartParams = StartLiveLocationShareTask.Params(
                 roomId = A_ROOM_ID,
                 timeoutMillis = A_TIMEOUT,
-                description = A_DESCRIPTION
         )
         coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
     }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
index aa8826243fcf7050fe934f4421f27cf6833bc4be..a5c126cf72ea47b07b9c908341a990f4c9078b66 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
@@ -34,7 +34,6 @@ import org.matrix.android.sdk.test.fakes.FakeSendStateTask
 private const val A_USER_ID = "user-id"
 private const val A_ROOM_ID = "room-id"
 private const val AN_EVENT_ID = "event-id"
-private const val A_DESCRIPTION = "description"
 private const val A_TIMEOUT = 15_000L
 private const val AN_EPOCH = 1655210176L
 
@@ -60,7 +59,6 @@ internal class DefaultStartLiveLocationShareTaskTest {
         val params = StartLiveLocationShareTask.Params(
                 roomId = A_ROOM_ID,
                 timeoutMillis = A_TIMEOUT,
-                description = A_DESCRIPTION
         )
         fakeClock.givenEpoch(AN_EPOCH)
         fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
@@ -69,7 +67,7 @@ internal class DefaultStartLiveLocationShareTaskTest {
 
         result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
         val expectedBeaconContent = MessageBeaconInfoContent(
-                body = A_DESCRIPTION,
+                body = "Live location",
                 timeout = params.timeoutMillis,
                 isLive = true,
                 unstableTimestampMillis = AN_EPOCH
@@ -77,7 +75,7 @@ internal class DefaultStartLiveLocationShareTaskTest {
         val expectedParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = A_USER_ID,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
                 body = expectedBeaconContent
         )
         fakeSendStateTask.verifyExecuteRetry(
@@ -91,7 +89,6 @@ internal class DefaultStartLiveLocationShareTaskTest {
         val params = StartLiveLocationShareTask.Params(
                 roomId = A_ROOM_ID,
                 timeoutMillis = A_TIMEOUT,
-                description = A_DESCRIPTION
         )
         fakeClock.givenEpoch(AN_EPOCH)
         fakeSendStateTask.givenExecuteRetryReturns("")
@@ -106,7 +103,6 @@ internal class DefaultStartLiveLocationShareTaskTest {
         val params = StartLiveLocationShareTask.Params(
                 roomId = A_ROOM_ID,
                 timeoutMillis = A_TIMEOUT,
-                description = A_DESCRIPTION
         )
         fakeClock.givenEpoch(AN_EPOCH)
         val error = Throwable()
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
index 1abf179ccf3bacec5c676c73d8b765a0eab6819e..a7adadfc63b34f72dc887872a64152ef1a5981de 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
@@ -79,7 +79,7 @@ class DefaultStopLiveLocationShareTaskTest {
         val expectedSendParams = SendStateTask.Params(
                 roomId = params.roomId,
                 stateKey = A_USER_ID,
-                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                eventType = EventType.STATE_ROOM_BEACON_INFO.stable,
                 body = expectedBeaconContent
         )
         fakeSendStateTask.verifyExecuteRetry(
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt
index 24d9c30039f497188ae6bf583e62a9ae7d70d387..d6edb69d93e1689abfab8744760dc0484642cee4 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt
@@ -79,7 +79,7 @@ class LiveLocationShareRedactionEventProcessorTest {
     @Test
     fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest {
         val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID)
-        val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.first())
+        val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.stable)
         fakeRealm.givenWhere<EventEntity>()
                 .givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
                 .givenFindFirst(redactedEventEntity)
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt
index b30428e5e101c02a10ae7805ffba9a7776c3c8e5..19f58d690ff7ac48999e499319a2c71a3c7acdd6 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt
@@ -23,8 +23,6 @@ import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.events.model.toModel
-import org.matrix.android.sdk.api.session.room.model.EditAggregatedSummary
-import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody
 import org.matrix.android.sdk.api.session.room.sender.SenderInfo
@@ -226,16 +224,14 @@ class LocalEchoEventFactoryTests {
             ).toMessageTextContent().toContent()
         }
         return TimelineEvent(
-                root = A_START_EVENT,
+                root = A_START_EVENT.copy(
+                        type = EventType.MESSAGE,
+                        content = textContent
+                ),
                 localId = 1234,
                 eventId = AN_EVENT_ID,
                 displayIndex = 0,
                 senderInfo = SenderInfo(A_USER_ID_1, A_USER_ID_1, true, null),
-                annotations = if (textContent != null) {
-                    EventAnnotationsSummary(
-                            editSummary = EditAggregatedSummary(latestContent = textContent, emptyList(), emptyList())
-                    )
-                } else null
         )
     }
 }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..201423685ca844e390bfb867ba0fa2c3c258e878
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/sync/DefaultGetCurrentFilterTaskTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.sync
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.api.session.sync.filter.SyncFilterBuilder
+import org.matrix.android.sdk.internal.session.filter.DefaultGetCurrentFilterTask
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+import org.matrix.android.sdk.test.fakes.FakeFilterRepository
+import org.matrix.android.sdk.test.fakes.FakeHomeServerCapabilitiesDataSource
+import org.matrix.android.sdk.test.fakes.FakeSaveFilterTask
+
+private const val A_FILTER_ID = "filter-id"
+private val A_HOMESERVER_CAPABILITIES = HomeServerCapabilities()
+private val A_SYNC_FILTER_PARAMS = SyncFilterParams(
+        lazyLoadMembersForMessageEvents = true,
+        lazyLoadMembersForStateEvents = true,
+        useThreadNotifications = true
+)
+
+@ExperimentalCoroutinesApi
+class DefaultGetCurrentFilterTaskTest {
+
+    private val filterRepository = FakeFilterRepository()
+    private val homeServerCapabilitiesDataSource = FakeHomeServerCapabilitiesDataSource()
+    private val saveFilterTask = FakeSaveFilterTask()
+
+    private val getCurrentFilterTask = DefaultGetCurrentFilterTask(
+            filterRepository = filterRepository,
+            homeServerCapabilitiesDataSource = homeServerCapabilitiesDataSource.instance,
+            saveFilterTask = saveFilterTask
+    )
+
+    @Test
+    fun `given no filter is stored, when execute, then executes task to save new filter`() = runTest {
+        filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
+
+        homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
+
+        filterRepository.givenFilterStored(null, null)
+
+        getCurrentFilterTask.execute(Unit)
+
+        val filter = SyncFilterBuilder()
+                .with(A_SYNC_FILTER_PARAMS)
+                .build(A_HOMESERVER_CAPABILITIES)
+
+        saveFilterTask.verifyExecution(filter)
+    }
+
+    @Test
+    fun `given filter is stored and didn't change, when execute, then returns stored filter id`() = runTest {
+        filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
+
+        homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
+
+        val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES)
+        filterRepository.givenFilterStored(A_FILTER_ID, filter.toJSONString())
+
+        val result = getCurrentFilterTask.execute(Unit)
+
+        result shouldBeEqualTo A_FILTER_ID
+    }
+
+    @Test
+    fun `given filter is set and home server capabilities has changed, when execute, then executes task to save new filter`() = runTest {
+        filterRepository.givenFilterParamsAreStored(A_SYNC_FILTER_PARAMS)
+
+        homeServerCapabilitiesDataSource.givenHomeServerCapabilities(A_HOMESERVER_CAPABILITIES)
+
+        val filter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(A_HOMESERVER_CAPABILITIES)
+        filterRepository.givenFilterStored(A_FILTER_ID, filter.toJSONString())
+
+        val newHomeServerCapabilities = HomeServerCapabilities(canUseThreadReadReceiptsAndNotifications = true)
+        homeServerCapabilitiesDataSource.givenHomeServerCapabilities(newHomeServerCapabilities)
+        val newFilter = SyncFilterBuilder().with(A_SYNC_FILTER_PARAMS).build(newHomeServerCapabilities)
+
+        getCurrentFilterTask.execute(Unit)
+
+        saveFilterTask.verifyExecution(newFilter)
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b8225f21d66cf9ec11fa76ec4135b7e322e196d2
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeFilterRepository.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.filter.FilterRepository
+import org.matrix.android.sdk.internal.sync.filter.SyncFilterParams
+
+internal class FakeFilterRepository : FilterRepository by mockk() {
+
+    fun givenFilterStored(filterId: String?, filterBody: String?) {
+        coEvery { getStoredSyncFilterId() } returns filterId
+        coEvery { getStoredSyncFilterBody() } returns filterBody
+    }
+
+    fun givenFilterParamsAreStored(syncFilterParams: SyncFilterParams?) {
+        coEvery { getStoredFilterParams() } returns syncFilterParams
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9a56a599d1610a50e198220928484b350ccf60d5
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeHomeServerCapabilitiesDataSource.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesDataSource
+
+internal class FakeHomeServerCapabilitiesDataSource {
+    val instance = mockk<HomeServerCapabilitiesDataSource>()
+
+    fun givenHomeServerCapabilities(homeServerCapabilities: HomeServerCapabilities) {
+        every { instance.getHomeServerCapabilities() } returns homeServerCapabilities
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
index 93999458c68caa0d72d09fb591324cc67792de8c..76ede759102202d434af3cc0f482f66ec5917db0 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
@@ -38,9 +38,9 @@ internal class FakeMonarchy {
     init {
         mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
         coEvery {
-            instance.awaitTransaction(any<suspend (Realm) -> Any>())
-        } coAnswers {
-            secondArg<suspend (Realm) -> Any>().invoke(fakeRealm.instance)
+            instance.awaitTransaction(any<(Realm) -> Any>())
+        } answers {
+            secondArg<(Realm) -> Any>().invoke(fakeRealm.instance)
         }
         coEvery {
             instance.doWithRealm(any())
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt
index 15a9823c79daddcddd0a6c1b72cfc8201a95be2f..9ad7032262c51810daa52c64ea58ea730d069da1 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt
@@ -19,7 +19,6 @@ package org.matrix.android.sdk.test.fakes
 import io.mockk.coEvery
 import io.mockk.mockk
 import io.mockk.mockkStatic
-import io.mockk.slot
 import io.realm.Realm
 import io.realm.RealmConfiguration
 import org.matrix.android.sdk.internal.database.awaitTransaction
@@ -33,9 +32,8 @@ internal class FakeRealmConfiguration {
     val instance = mockk<RealmConfiguration>()
 
     fun <T> givenAwaitTransaction(realm: Realm) {
-        val transaction = slot<suspend (Realm) -> T>()
-        coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers {
-            secondArg<suspend (Realm) -> T>().invoke(realm)
+        coEvery { awaitTransaction(instance, any<(Realm) -> T>()) } answers {
+            secondArg<(Realm) -> T>().invoke(realm)
         }
     }
 }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..40bee227e0f07386b7ab41b7afd6c3ceb90c3fcb
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSaveFilterTask.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import io.mockk.slot
+import org.amshove.kluent.shouldBeEqualTo
+import org.matrix.android.sdk.internal.session.filter.Filter
+import org.matrix.android.sdk.internal.session.filter.SaveFilterTask
+import java.util.UUID
+
+internal class FakeSaveFilterTask : SaveFilterTask by mockk() {
+
+    init {
+        coEvery { execute(any()) } returns UUID.randomUUID().toString()
+    }
+
+    fun verifyExecution(filter: Filter) {
+        val slot = slot<SaveFilterTask.Params>()
+        coVerify { execute(capture(slot)) }
+        val params = slot.captured
+        params.filter shouldBeEqualTo filter
+    }
+}