diff --git a/.gitignore b/.gitignore
index 56cc6425e036a5ea3738290ca3bf5adccad671ad..f100b28a3a4af1769182bc2281c41fc0883fa057 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,3 +83,5 @@ lint/generated/
 lint/outputs/
 lint/tmp/
 # lint/reports/
+
+/tmp
diff --git a/CHANGES.md b/CHANGES.md
index 26253bab8cba13535642a15bb354531b61eab1b2..94ccd870ddbf3e4b12f0281364ceb835411d51b3 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,16 @@
 Please also refer to the Changelog of Element Android: https://github.com/vector-im/element-android/blob/main/CHANGES.md
 
+Changes in Matrix-SDK v1.5.8 (2022-11-23)
+=========================================
+
+Imported from Element 1.5.8. (https://github.com/vector-im/element-android/releases/tag/v1.5.8)
+
+SDK API changes ⚠️
+------------------
+- [Metrics] Add `SpannableMetricPlugin` to support spans within transactions. ([#7514](https://github.com/vector-im/element-android/issues/7514))
+- Fix a bug that caused messages with no formatted text to be quoted as "null". ([#7530](https://github.com/vector-im/element-android/issues/7530))
+- If message content has no `formattedBody`, default to `body` when editing. ([#7574](https://github.com/vector-im/element-android/issues/7574))
+
 Changes in Matrix-SDK v1.5.7 (2022-11-16)
 =======================================
 
diff --git a/dependencies.gradle b/dependencies.gradle
index 33a2096a4344e5d0d3da5fe175520378a9f58f15..dc66de43eac191375fff013b9fce6ea655b9249e 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -8,7 +8,7 @@ ext.versions = [
 
 def gradle = "7.3.1"
 // Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.7.20"
+def kotlin = "1.7.21"
 def kotlinCoroutines = "1.6.4"
 def dagger = "2.44"
 def appDistribution = "16.0.0-beta05"
@@ -17,7 +17,7 @@ def markwon = "4.6.2"
 def moshi = "1.14.0"
 def lifecycle = "2.5.1"
 def flowBinding = "1.2.0"
-def flipper = "0.171.1"
+def flipper = "0.174.0"
 def epoxy = "5.0.0"
 def mavericks = "3.0.1"
 def glide = "4.14.2"
@@ -26,13 +26,13 @@ def jjwt = "0.11.5"
 // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
 // the whole commit which set version 0.16.0-SNAPSHOT
 def vanniktechEmoji = "0.16.0-SNAPSHOT"
-def sentry = "6.6.0"
+def sentry = "6.7.0"
 def fragment = "1.5.4"
 // Testing
 def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
 def espresso = "3.4.0"
 def androidxTest = "1.4.0"
-def androidxOrchestrator = "1.4.1"
+def androidxOrchestrator = "1.4.2"
 def paparazzi = "1.1.0"
 
 ext.libs = [
@@ -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.12.57"
+                'phonenumber'             : "com.googlecode.libphonenumber:libphonenumber:8.13.0"
         ],
         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.2.1"
+                'wysiwyg'                 : "io.element.android:wysiwyg:0.4.0"
         ],
         squareup    : [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",
diff --git a/gradle.properties b/gradle.properties
index d07487a737c0a9d959ac4064a4a24503a652068d..ca7f12413ae82ba2c898167d556b5e33720c41d1 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -26,7 +26,7 @@ vector.httpLogLevel=NONE
 # Ref: https://github.com/vanniktech/gradle-maven-publish-plugin
 GROUP=org.matrix.android
 POM_ARTIFACT_ID=matrix-android-sdk2
-VERSION_NAME=1.5.7
+VERSION_NAME=1.5.8
 
 POM_PACKAGING=aar
 
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 30b7b194017be8e4c3f97db7803620360e65a72a..d08c176070cd467f245426b853b33e73d88d23e0 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -4,3 +4,4 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
 distributionPath=wrapper/dists
 zipStorePath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
index 6ef90193d8010f76a15f86c6bd996fdf3dc8859a..81351523e93311296dedf9d01aecac9c713a74ee 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.session.search
 
+import org.amshove.kluent.shouldBeEqualTo
 import org.junit.Assert.assertTrue
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -43,7 +44,7 @@ class SearchMessagesTest : InstrumentedTest {
             cryptoTestData.firstSession
                     .searchService()
                     .search(
-                            searchTerm = "lore",
+                            searchTerm = "lorem",
                             limit = 10,
                             includeProfile = true,
                             afterLimit = 0,
@@ -61,7 +62,7 @@ class SearchMessagesTest : InstrumentedTest {
             cryptoTestData.firstSession
                     .searchService()
                     .search(
-                            searchTerm = "lore",
+                            searchTerm = "lorem",
                             roomId = cryptoTestData.roomId,
                             limit = 10,
                             includeProfile = true,
@@ -73,7 +74,28 @@ class SearchMessagesTest : InstrumentedTest {
         }
     }
 
-    private fun doTest(block: suspend (CryptoTestData) -> SearchResult) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
+    @Test
+    fun sendTextMessageAndSearchPartOfItIncompleteWord() {
+        doTest(expectedNumberOfResult = 0) { cryptoTestData ->
+            cryptoTestData.firstSession
+                    .searchService()
+                    .search(
+                            searchTerm = "lore", /* incomplete word */
+                            roomId = cryptoTestData.roomId,
+                            limit = 10,
+                            includeProfile = true,
+                            afterLimit = 0,
+                            beforeLimit = 10,
+                            orderByRecent = true,
+                            nextBatch = null
+                    )
+        }
+    }
+
+    private fun doTest(
+            expectedNumberOfResult: Int = 2,
+            block: suspend (CryptoTestData) -> SearchResult,
+    ) = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
         val aliceSession = cryptoTestData.firstSession
         val aliceRoomId = cryptoTestData.roomId
@@ -87,7 +109,7 @@ class SearchMessagesTest : InstrumentedTest {
 
         val data = block.invoke(cryptoTestData)
 
-        assertTrue(data.results?.size == 2)
+        data.results?.size shouldBeEqualTo expectedNumberOfResult
         assertTrue(
                 data.results
                         ?.all {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt
index 9487a270863047d41d2c4dcc23311ebdaea6e04b..7f0e828f628e517e2ff88c71e0ea3f3d37a969a8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MetricsExtensions.kt
@@ -17,25 +17,51 @@
 package org.matrix.android.sdk.api.extensions
 
 import org.matrix.android.sdk.api.metrics.MetricPlugin
+import org.matrix.android.sdk.api.metrics.SpannableMetricPlugin
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
 
 /**
  * Executes the given [block] while measuring the transaction.
+ *
+ * @param block Action/Task to be executed within this span.
+ */
+@OptIn(ExperimentalContracts::class)
+inline fun List<MetricPlugin>.measureMetric(block: () -> Unit) {
+    contract {
+        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+    }
+    try {
+        this.forEach { plugin -> plugin.startTransaction() } // Start the transaction.
+        block()
+    } catch (throwable: Throwable) {
+        this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
+        throw throwable
+    } finally {
+        this.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction.
+    }
+}
+
+/**
+ * Executes the given [block] while measuring a span.
+ *
+ * @param operation Name of the new span.
+ * @param description Description of the new span.
+ * @param block Action/Task to be executed within this span.
  */
 @OptIn(ExperimentalContracts::class)
-inline fun measureMetric(metricMeasurementPlugins: List<MetricPlugin>, block: () -> Unit) {
+inline fun List<SpannableMetricPlugin>.measureSpan(operation: String, description: String, block: () -> Unit) {
     contract {
         callsInPlace(block, InvocationKind.EXACTLY_ONCE)
     }
     try {
-        metricMeasurementPlugins.forEach { plugin -> plugin.startTransaction() } // Start the transaction.
+        this.forEach { plugin -> plugin.startSpan(operation, description) } // Start the transaction.
         block()
     } catch (throwable: Throwable) {
-        metricMeasurementPlugins.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
+        this.forEach { plugin -> plugin.onError(throwable) } // Capture if there is any exception thrown.
         throw throwable
     } finally {
-        metricMeasurementPlugins.forEach { plugin -> plugin.finishTransaction() } // Finally, finish this transaction.
+        this.forEach { plugin -> plugin.finishSpan() } // Finally, finish this transaction.
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt
new file mode 100644
index 0000000000000000000000000000000000000000..54aa21877ec8e38ac2efe718eff3a6251acbd69d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SpannableMetricPlugin.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.metrics
+
+/**
+ * A plugin that tracks span along with transactions.
+ */
+interface SpannableMetricPlugin : MetricPlugin {
+
+    /**
+     * Starts the span for a sub-task.
+     *
+     * @param operation Name of the new span.
+     * @param description Description of the new span.
+     */
+    fun startSpan(operation: String, description: String)
+
+    /**
+     * Finish the span when sub-task is completed.
+     */
+    fun finishSpan()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt
new file mode 100644
index 0000000000000000000000000000000000000000..79ece002e9851b1e4a8deaac857d3c4e3147329a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/metrics/SyncDurationMetricPlugin.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.metrics
+
+import org.matrix.android.sdk.api.logger.LoggerTag
+import timber.log.Timber
+
+private val loggerTag = LoggerTag("SyncDurationMetricPlugin", LoggerTag.CRYPTO)
+
+/**
+ * An spannable metric plugin for sync response handling task.
+ */
+interface SyncDurationMetricPlugin : SpannableMetricPlugin {
+
+    override fun logTransaction(message: String?) {
+        Timber.tag(loggerTag.value).v("## syncResponseHandler() : $message")
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
index d2aa8020e8257abbd48807a884a9450afa7c02c5..971d04261eb046cbfe87f8f45058f2d192177f14 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.api.session.crypto
 
 import android.content.Context
+import androidx.annotation.Size
 import androidx.lifecycle.LiveData
 import androidx.paging.PagedList
 import org.matrix.android.sdk.api.MatrixCallback
@@ -55,6 +56,8 @@ interface CryptoService {
 
     fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>)
 
+    fun deleteDevices(@Size(min = 1) deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>)
+
     fun getCryptoVersion(context: Context, longFormat: Boolean): String
 
     fun isCryptoEnabled(): Boolean
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 1f16041b543f480834d2775aae7e9997bc0d9c8f..6ae585a27325a22529beca37add9090e14836e12 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
@@ -53,7 +53,7 @@ inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
     val moshiAdapter = moshi.adapter(T::class.java)
     return try {
         moshiAdapter.fromJsonValue(this)
-    } catch (e: Exception) {
+    } catch (e: Throwable) {
         if (catchError) {
             Timber.e(e, "To model failed : $e")
             null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
index 773e870ffd9e329d274849dab89a04c73afad283..11638837ccdeb47d77d133fed441b317e0499251 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
@@ -70,6 +70,11 @@ data class HomeServerCapabilities(
          * True if the home server supports threaded read receipts and unread notifications.
          */
         val canUseThreadReadReceiptsAndNotifications: Boolean = false,
+
+        /**
+         * True if the home server supports remote toggle of Pusher for a given device.
+         */
+        val canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
 ) {
 
     enum class RoomCapabilitySupport {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt
index 9d2c48e194f930a7f47e0f5642a7f602b9e59f76..c65a5382fb805cd3e0740df458767cb72468460e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilitiesService.kt
@@ -16,6 +16,9 @@
 
 package org.matrix.android.sdk.api.session.homeserver
 
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.util.Optional
+
 /**
  * This interface defines a method to retrieve the homeserver capabilities.
  */
@@ -30,4 +33,9 @@ interface HomeServerCapabilitiesService {
      * Get the HomeServer capabilities.
      */
     fun getHomeServerCapabilities(): HomeServerCapabilities
+
+    /**
+     * Get a LiveData on the HomeServer capabilities.
+     */
+    fun getHomeServerCapabilitiesLive(): LiveData<Optional<HomeServerCapabilities>>
 }
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 223acd1b9c6bff98e50ba829053d3a39ed7ffe69..6f4049de364ba9e9a617039542e092190c7c9998 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
@@ -180,11 +180,13 @@ fun TimelineEvent.isRootThread(): Boolean {
 
 /**
  * Get the latest message body, after a possible edition, stripping the reply prefix if necessary.
+ * @param formatted Indicates whether the formatted HTML body of the message should be retrieved of the plain text one.
+ * @return If [formatted] is `true`, the HTML body of the message will be retrieved if available. Otherwise, the plain text/markdown version will be returned.
  */
 fun TimelineEvent.getTextEditableContent(formatted: Boolean): String {
     val lastMessageContent = getLastMessageContent()
     val lastContentBody = if (formatted && lastMessageContent is MessageContentWithFormattedBody) {
-        lastMessageContent.formattedBody
+        lastMessageContent.formattedBody ?: lastMessageContent.body
     } else {
         lastMessageContent?.body
     } ?: return ""
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
index 1245d8df4b6b1d4d0b1399cc5c80edf5e06c04ef..f4de6a9ae945fce1de76251cc3c24567f3d2d367 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.version
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.extensions.orFalse
 
 /**
  * Model for https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions.
@@ -56,6 +57,7 @@ private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable"
 private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882"
 private const val FEATURE_THREADS_MSC3771 = "org.matrix.msc3771"
 private const val FEATURE_THREADS_MSC3773 = "org.matrix.msc3773"
+private const val FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881 = "org.matrix.msc3881"
 
 /**
  * Return true if the SDK supports this homeserver version.
@@ -142,3 +144,12 @@ private fun Versions.getMaxVersion(): HomeServerVersion {
             ?.maxOrNull()
             ?: HomeServerVersion.r0_0_0
 }
+
+/**
+ * Indicate if the server supports MSC3881: https://github.com/matrix-org/matrix-spec-proposals/pull/3881.
+ *
+ * @return true if remote toggle of push notifications is supported
+ */
+internal fun Versions.doesServerSupportRemoteToggleOfPushNotifications(): Boolean {
+    return unstableFeatures?.get(FEATURE_REMOTE_TOGGLE_PUSH_NOTIFICATIONS_MSC3881).orFalse()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
index 9c3e0ba1c588be1261e842632180ace421220cb2..7862da1c17144ea9e1aa16250eae857d7c536080 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
@@ -242,8 +242,12 @@ internal class DefaultCryptoService @Inject constructor(
     }
 
     override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
+        deleteDevices(listOf(deviceId), userInteractiveAuthInterceptor, callback)
+    }
+
+    override fun deleteDevices(deviceIds: List<String>, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
         deleteDeviceTask
-                .configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) {
+                .configureWith(DeleteDeviceTask.Params(deviceIds, userInteractiveAuthInterceptor, null)) {
                     this.executionThread = TaskThread.CRYPTO
                     this.callback = callback
                 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
index 2ac6b8c85413fd86feb035752db69ca9df2bcbf0..7e9e156003c4cfed01d17c17c4badedba07ac0fd 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
@@ -355,7 +355,7 @@ internal class DeviceListManager @Inject constructor(
         val relevantPlugins = metricPlugins.filterIsInstance<DownloadDeviceKeysMetricsPlugin>()
 
         val response: KeysQueryResponse
-        measureMetric(relevantPlugins) {
+        relevantPlugins.measureMetric {
             response = try {
                 downloadKeysForUsersTask.execute(params)
             } catch (throwable: Throwable) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt
index d5a8bdfd7cb62f17b4be13f66a34b18a7c6df820..cfe4681bfd53ea01c9de3cd8a214e38dfa7785d6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.api
 import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
 import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
 import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
+import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams
 import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse
 import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody
 import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse
@@ -136,6 +137,17 @@ internal interface CryptoApi {
             @Body params: DeleteDeviceParams
     )
 
+    /**
+     * Deletes the given devices, and invalidates any access token associated with them.
+     * Doc: https://spec.matrix.org/v1.4/client-server-api/#post_matrixclientv3delete_devices
+     *
+     * @param params the deletion parameters
+     */
+    @POST(NetworkConstants.URI_API_PREFIX_PATH_V3 + "delete_devices")
+    suspend fun deleteDevices(
+            @Body params: DeleteDevicesParams
+    )
+
     /**
      * Update the device information.
      * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt
index c26c6107c4eeb53fa346e4b969d13123d787ec01..24dccc4d9040bcc2454e3517fd4c262c9fb88dc7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt
@@ -23,6 +23,9 @@ import com.squareup.moshi.JsonClass
  */
 @JsonClass(generateAdapter = true)
 internal data class DeleteDeviceParams(
+        /**
+         * Additional authentication information for the user-interactive authentication API.
+         */
         @Json(name = "auth")
-        val auth: Map<String, *>? = null
+        val auth: Map<String, *>? = null,
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.kt
new file mode 100644
index 0000000000000000000000000000000000000000..19b33b2a69195f1717505e0e732a8671c26d24e0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDevicesParams.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.internal.crypto.model.rest
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * This class provides the parameter to delete several devices.
+ */
+@JsonClass(generateAdapter = true)
+internal data class DeleteDevicesParams(
+        /**
+         * Additional authentication information for the user-interactive authentication API.
+         */
+        @Json(name = "auth")
+        val auth: Map<String, *>? = null,
+
+        /**
+         * Required: The list of device IDs to delete.
+         */
+        @Json(name = "devices")
+        val deviceIds: List<String>,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt
index 0a77d33accc9710bacbd3233cecc4b4146d72e58..549122447e9bb965d3c698284879f0998dd54352 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt
@@ -16,12 +16,14 @@
 
 package org.matrix.android.sdk.internal.crypto.tasks
 
+import androidx.annotation.Size
 import org.matrix.android.sdk.api.auth.UIABaseAuth
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.session.uia.UiaResult
 import org.matrix.android.sdk.internal.auth.registration.handleUIA
 import org.matrix.android.sdk.internal.crypto.api.CryptoApi
 import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
+import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDevicesParams
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
@@ -30,7 +32,7 @@ import javax.inject.Inject
 
 internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
     data class Params(
-            val deviceId: String,
+            @Size(min = 1) val deviceIds: List<String>,
             val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
             val userAuthParam: UIABaseAuth?
     )
@@ -42,9 +44,24 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
 ) : DeleteDeviceTask {
 
     override suspend fun execute(params: DeleteDeviceTask.Params) {
+        require(params.deviceIds.isNotEmpty())
+
         try {
             executeRequest(globalErrorReceiver) {
-                cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap()))
+                val userAuthParam = params.userAuthParam?.asMap()
+                if (params.deviceIds.size == 1) {
+                    cryptoApi.deleteDevice(
+                            deviceId = params.deviceIds.first(),
+                            DeleteDeviceParams(auth = userAuthParam)
+                    )
+                } else {
+                    cryptoApi.deleteDevices(
+                            DeleteDevicesParams(
+                                    auth = userAuthParam,
+                                    deviceIds = params.deviceIds
+                            )
+                    )
+                }
             }
         } catch (throwable: Throwable) {
             if (params.userInteractiveAuthInterceptor == null ||
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 58c015b13b63c4ac9f98f6c82980f6e83ed912eb..30836c027ea60291c8b1a06e7f4d30d482a0b22c 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
@@ -58,6 +58,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo038
 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.util.Normalizer
 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import javax.inject.Inject
@@ -66,7 +67,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
         private val normalizer: Normalizer
 ) : MatrixRealmMigration(
         dbName = "Session",
-        schemaVersion = 41L,
+        schemaVersion = 42L,
 ) {
     /**
      * Forces all RealmSessionStoreMigration instances to be equal.
@@ -117,5 +118,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 39) MigrateSessionTo039(realm).perform()
         if (oldVersion < 40) MigrateSessionTo040(realm).perform()
         if (oldVersion < 41) MigrateSessionTo041(realm).perform()
+        if (oldVersion < 42) MigrateSessionTo042(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
index 3528ca0051de21b6f207201e1e95f4b6d8ec6966..89657ad8822867085f97ef4240683bd3107e3a4a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
@@ -45,7 +45,8 @@ internal object HomeServerCapabilitiesMapper {
                 canUseThreading = entity.canUseThreading,
                 canControlLogoutDevices = entity.canControlLogoutDevices,
                 canLoginWithQrCode = entity.canLoginWithQrCode,
-                canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications
+                canUseThreadReadReceiptsAndNotifications = entity.canUseThreadReadReceiptsAndNotifications,
+                canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices,
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8826d894c101e6aed1015008b3df6930e5111656
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo042.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo042(realm: DynamicRealm) : RealmMigrator(realm, 42) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addField(HomeServerCapabilitiesEntityFields.CAN_REMOTELY_TOGGLE_PUSH_NOTIFICATIONS_OF_DEVICES, Boolean::class.java)
+                ?.forceRefreshOfHomeServerCapabilities()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
index 89f1e50b30997272cb7babaa25ae0862798cabb4..2b60f7723cbbb1aac3337f964481f4f456869ecb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
@@ -33,6 +33,7 @@ internal open class HomeServerCapabilitiesEntity(
         var canControlLogoutDevices: Boolean = false,
         var canLoginWithQrCode: Boolean = false,
         var canUseThreadReadReceiptsAndNotifications: Boolean = false,
+        var canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
 ) : RealmObject() {
 
     companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
index 5aec7db66cc08848103e15bf8b11d081b20c4299..4bfda0bf3cdde722a1505f7b90f686cff92d1db8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
@@ -22,6 +22,7 @@ internal object NetworkConstants {
     const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
     const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
     const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/"
+    const val URI_API_PREFIX_PATH_V3 = "$URI_API_PREFIX_PATH/v3/"
     const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
 
     // Media
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
index db1cd1b33bbf662757a19732ec29e2bfde65aa77..3dd440737ad693991b7d14cc812ab9b3623f6efd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
@@ -408,7 +408,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
             newAttachmentAttributes: NewAttachmentAttributes
     ) {
         localEchoRepository.updateEcho(eventId) { _, event ->
-            val content: Content? = event.asDomain().content
+            val content: Content? = event.asDomain(castJsonNumbers = true).content
             val messageContent: MessageContent? = content.toModel()
             // Retrieve potential additional content from the original event
             val additionalContent = content.orEmpty() - messageContent?.toContent().orEmpty().keys
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt
index 4c755b54b514cf8f8fd2d50c412e2b5439114814..eb9e862de295d65c87f7806ea303a0597de30bdc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/DefaultHomeServerCapabilitiesService.kt
@@ -16,8 +16,10 @@
 
 package org.matrix.android.sdk.internal.session.homeserver
 
+import androidx.lifecycle.LiveData
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
+import org.matrix.android.sdk.api.util.Optional
 import javax.inject.Inject
 
 internal class DefaultHomeServerCapabilitiesService @Inject constructor(
@@ -33,4 +35,8 @@ internal class DefaultHomeServerCapabilitiesService @Inject constructor(
         return homeServerCapabilitiesDataSource.getHomeServerCapabilities()
                 ?: HomeServerCapabilities()
     }
+
+    override fun getHomeServerCapabilitiesLive(): LiveData<Optional<HomeServerCapabilities>> {
+        return homeServerCapabilitiesDataSource.getHomeServerCapabilitiesLive()
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index a5953d870c80366a82f0d1c89bd710d43f4fee96..11e86a5c513176d9f39a33d82ddc916fc454d335 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 import org.matrix.android.sdk.internal.auth.version.Versions
 import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
 import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportRemoteToggleOfPushNotifications
 import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreadUnreadNotifications
 import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
 import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
@@ -141,13 +142,18 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
             }
 
             if (getVersionResult != null) {
-                homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk()
-                homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices()
+                homeServerCapabilitiesEntity.lastVersionIdentityServerSupported =
+                        getVersionResult.isLoginAndRegistrationSupportedBySdk()
+                homeServerCapabilitiesEntity.canControlLogoutDevices =
+                        getVersionResult.doesServerSupportLogoutDevices()
                 homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */
                         getVersionResult.doesServerSupportThreads()
                 homeServerCapabilitiesEntity.canUseThreadReadReceiptsAndNotifications =
                         getVersionResult.doesServerSupportThreadUnreadNotifications()
-                homeServerCapabilitiesEntity.canLoginWithQrCode = getVersionResult.doesServerSupportQrCodeLogin()
+                homeServerCapabilitiesEntity.canLoginWithQrCode =
+                        getVersionResult.doesServerSupportQrCodeLogin()
+                homeServerCapabilitiesEntity.canRemotelyTogglePushNotificationsOfDevices =
+                        getVersionResult.doesServerSupportRemoteToggleOfPushNotifications()
             }
 
             if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt
index 6c913fa41ee6b16feb8e9e0da0ed73f294cb422b..beb1e67e40929c46ea5acb438fd4bb6d760ad713 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/HomeServerCapabilitiesDataSource.kt
@@ -16,9 +16,14 @@
 
 package org.matrix.android.sdk.internal.session.homeserver
 
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Transformations
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
+import io.realm.kotlin.where
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
 import org.matrix.android.sdk.internal.database.mapper.HomeServerCapabilitiesMapper
 import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
 import org.matrix.android.sdk.internal.database.query.get
@@ -26,7 +31,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
 import javax.inject.Inject
 
 internal class HomeServerCapabilitiesDataSource @Inject constructor(
-        @SessionDatabase private val monarchy: Monarchy
+        @SessionDatabase private val monarchy: Monarchy,
 ) {
     fun getHomeServerCapabilities(): HomeServerCapabilities? {
         return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
@@ -35,4 +40,14 @@ internal class HomeServerCapabilitiesDataSource @Inject constructor(
             }
         }
     }
+
+    fun getHomeServerCapabilitiesLive(): LiveData<Optional<HomeServerCapabilities>> {
+        val liveData = monarchy.findAllMappedWithChanges(
+                { realm: Realm -> realm.where<HomeServerCapabilitiesEntity>() },
+                { HomeServerCapabilitiesMapper.map(it) }
+        )
+        return Transformations.map(liveData) {
+            it.firstOrNull().toOptional()
+        }
+    }
 }
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 7d8605c2bd399abd1291dcdc4eaacaabefb4abaa..55ba78c2a59dd5811d92b332c4d47b3a5ad07b3e 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
@@ -804,20 +804,12 @@ internal class LocalEchoEventFactory @Inject constructor(
             additionalContent: Content? = null,
     ): Event {
         val messageContent = quotedEvent.getLastMessageContent()
-        val textMsg = if (messageContent is MessageContentWithFormattedBody) {
-            messageContent.formattedBody
-        } else {
-            messageContent?.body
-        }
-        val quoteText = legacyRiotQuoteText(textMsg, text)
-        val quoteFormattedText = "<blockquote>$textMsg</blockquote>$formattedText"
-
+        val formattedQuotedText = (messageContent as? MessageContentWithFormattedBody)?.formattedBody
+        val textContent = createQuoteTextContent(messageContent?.body, formattedQuotedText, text, formattedText, autoMarkdown)
         return if (rootThreadEventId != null) {
             createMessageEvent(
                     roomId,
-                    markdownParser
-                            .parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText)
-                            .toThreadTextContent(
+                    textContent.toThreadTextContent(
                                     rootThreadEventId = rootThreadEventId,
                                     latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId),
                                     msgType = MessageType.MSGTYPE_TEXT
@@ -827,31 +819,54 @@ internal class LocalEchoEventFactory @Inject constructor(
         } else {
             createFormattedTextEvent(
                     roomId,
-                    markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText),
+                    textContent,
                     MessageType.MSGTYPE_TEXT,
                     additionalContent,
             )
         }
     }
 
-    private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
-        val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
-        return buildString {
-            if (messageParagraphs != null) {
-                for (i in messageParagraphs.indices) {
-                    if (messageParagraphs[i].isNotBlank()) {
-                        append("> ")
-                        append(messageParagraphs[i])
-                    }
-
-                    if (i != messageParagraphs.lastIndex) {
-                        append("\n\n")
-                    }
+    private fun createQuoteTextContent(
+            quotedText: String?,
+            formattedQuotedText: String?,
+            text: String,
+            formattedText: String?,
+            autoMarkdown: Boolean
+    ): TextContent {
+        val currentFormattedText = formattedText ?: if (autoMarkdown) {
+            val parsed = markdownParser.parse(text, force = true, advanced = true)
+            // If formattedText == text, formattedText is returned as null
+            parsed.formattedText ?: parsed.text
+        } else {
+            text
+        }
+        val processedFormattedQuotedText = formattedQuotedText ?: quotedText
+
+        val plainTextBody = buildString {
+            val plainMessageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray().orEmpty()
+            plainMessageParagraphs.forEachIndexed { index, paragraph ->
+                if (paragraph.isNotBlank()) {
+                    append("> ")
+                    append(paragraph)
+                }
+
+                if (index != plainMessageParagraphs.lastIndex) {
+                    append("\n\n")
                 }
             }
             append("\n\n")
-            append(myText)
+            append(text)
+        }
+        val formattedTextBody = buildString {
+            if (!processedFormattedQuotedText.isNullOrBlank()) {
+                append("<blockquote>")
+                append(processedFormattedQuotedText)
+                append("</blockquote>")
+            }
+            append("<br/>")
+            append(currentFormattedText)
         }
+        return TextContent(plainTextBody, formattedTextBody)
     }
 
     companion object {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
index 05216d1de132b3e0e16ae2ed8d8beaee0d722056..05d50d95952cc451e5de56775a249165c5c55496 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
@@ -17,6 +17,11 @@
 package org.matrix.android.sdk.internal.session.sync
 
 import com.zhuinden.monarchy.Monarchy
+import io.realm.Realm
+import org.matrix.android.sdk.api.MatrixConfiguration
+import org.matrix.android.sdk.api.extensions.measureMetric
+import org.matrix.android.sdk.api.extensions.measureSpan
+import org.matrix.android.sdk.api.metrics.SyncDurationMetricPlugin
 import org.matrix.android.sdk.api.session.pushrules.PushRuleService
 import org.matrix.android.sdk.api.session.pushrules.RuleScope
 import org.matrix.android.sdk.api.session.sync.InitialSyncStep
@@ -52,9 +57,12 @@ internal class SyncResponseHandler @Inject constructor(
         private val tokenStore: SyncTokenStore,
         private val processEventForPushTask: ProcessEventForPushTask,
         private val pushRuleService: PushRuleService,
-        private val presenceSyncHandler: PresenceSyncHandler
+        private val presenceSyncHandler: PresenceSyncHandler,
+        matrixConfiguration: MatrixConfiguration,
 ) {
 
+    private val relevantPlugins = matrixConfiguration.metricPlugins.filterIsInstance<SyncDurationMetricPlugin>()
+
     suspend fun handleResponse(
             syncResponse: SyncResponse,
             fromToken: String?,
@@ -63,39 +71,91 @@ internal class SyncResponseHandler @Inject constructor(
         val isInitialSync = fromToken == null
         Timber.v("Start handling sync, is InitialSync: $isInitialSync")
 
-        measureTimeMillis {
-            if (!cryptoService.isStarted()) {
-                Timber.v("Should start cryptoService")
-                cryptoService.start()
-            }
-            cryptoService.onSyncWillProcess(isInitialSync)
-        }.also {
-            Timber.v("Finish handling start cryptoService in $it ms")
+        relevantPlugins.measureMetric {
+            startCryptoService(isInitialSync)
+
+            // Handle the to device events before the room ones
+            // to ensure to decrypt them properly
+            handleToDevice(syncResponse, reporter)
+
+            val aggregator = SyncResponsePostTreatmentAggregator()
+
+            // Prerequisite for thread events handling in RoomSyncHandler
+            // Disabled due to the new fallback
+            //        if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
+            //            threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
+            //        }
+
+            startMonarchyTransaction(syncResponse, isInitialSync, reporter, aggregator)
+
+            aggregateSyncResponse(aggregator)
+
+            postTreatmentSyncResponse(syncResponse, isInitialSync)
+
+            markCryptoSyncCompleted(syncResponse)
+
+            handlePostSync()
+
+            Timber.v("On sync completed")
         }
+    }
 
-        // Handle the to device events before the room ones
-        // to ensure to decrypt them properly
-        measureTimeMillis {
-            Timber.v("Handle toDevice")
-            reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) {
-                if (syncResponse.toDevice != null) {
-                    cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter)
+    private fun startCryptoService(isInitialSync: Boolean) {
+        relevantPlugins.measureSpan("task", "start_crypto_service") {
+            measureTimeMillis {
+                if (!cryptoService.isStarted()) {
+                    Timber.v("Should start cryptoService")
+                    cryptoService.start()
                 }
+                cryptoService.onSyncWillProcess(isInitialSync)
+            }.also {
+                Timber.v("Finish handling start cryptoService in $it ms")
             }
-        }.also {
-            Timber.v("Finish handling toDevice in $it ms")
         }
-        val aggregator = SyncResponsePostTreatmentAggregator()
+    }
 
-        // Prerequisite for thread events handling in RoomSyncHandler
-// Disabled due to the new fallback
-//        if (!lightweightSettingsStorage.areThreadMessagesEnabled()) {
-//            threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse)
-//        }
+    private suspend fun handleToDevice(syncResponse: SyncResponse, reporter: ProgressReporter?) {
+        relevantPlugins.measureSpan("task", "handle_to_device") {
+            measureTimeMillis {
+                Timber.v("Handle toDevice")
+                reportSubtask(reporter, InitialSyncStep.ImportingAccountCrypto, 100, 0.1f) {
+                    if (syncResponse.toDevice != null) {
+                        cryptoSyncHandler.handleToDevice(syncResponse.toDevice, reporter)
+                    }
+                }
+            }.also {
+                Timber.v("Finish handling toDevice in $it ms")
+            }
+        }
+    }
 
+    private suspend fun startMonarchyTransaction(
+            syncResponse: SyncResponse,
+            isInitialSync: Boolean,
+            reporter: ProgressReporter?,
+            aggregator: SyncResponsePostTreatmentAggregator
+    ) {
         // Start one big transaction
-        monarchy.awaitTransaction { realm ->
-            // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local)
+        relevantPlugins.measureSpan("task", "monarchy_transaction") {
+            monarchy.awaitTransaction { realm ->
+                // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local)
+                handleRooms(reporter, syncResponse, realm, isInitialSync, aggregator)
+                handleAccountData(reporter, realm, syncResponse)
+                handlePresence(realm, syncResponse)
+
+                tokenStore.saveToken(realm, syncResponse.nextBatch)
+            }
+        }
+    }
+
+    private fun handleRooms(
+            reporter: ProgressReporter?,
+            syncResponse: SyncResponse,
+            realm: Realm,
+            isInitialSync: Boolean,
+            aggregator: SyncResponsePostTreatmentAggregator
+    ) {
+        relevantPlugins.measureSpan("task", "handle_rooms") {
             measureTimeMillis {
                 Timber.v("Handle rooms")
                 reportSubtask(reporter, InitialSyncStep.ImportingAccountRoom, 1, 0.8f) {
@@ -106,7 +166,11 @@ internal class SyncResponseHandler @Inject constructor(
             }.also {
                 Timber.v("Finish handling rooms in $it ms")
             }
+        }
+    }
 
+    private fun handleAccountData(reporter: ProgressReporter?, realm: Realm, syncResponse: SyncResponse) {
+        relevantPlugins.measureSpan("task", "handle_account_data") {
             measureTimeMillis {
                 reportSubtask(reporter, InitialSyncStep.ImportingAccountData, 1, 0.1f) {
                     Timber.v("Handle accountData")
@@ -115,44 +179,59 @@ internal class SyncResponseHandler @Inject constructor(
             }.also {
                 Timber.v("Finish handling accountData in $it ms")
             }
+        }
+    }
 
+    private fun handlePresence(realm: Realm, syncResponse: SyncResponse) {
+        relevantPlugins.measureSpan("task", "handle_presence") {
             measureTimeMillis {
                 Timber.v("Handle Presence")
                 presenceSyncHandler.handle(realm, syncResponse.presence)
             }.also {
                 Timber.v("Finish handling Presence in $it ms")
             }
-            tokenStore.saveToken(realm, syncResponse.nextBatch)
         }
+    }
 
-        // Everything else we need to do outside the transaction
-        measureTimeMillis {
-            aggregatorHandler.handle(aggregator)
-        }.also {
-            Timber.v("Aggregator management took $it ms")
+    private suspend fun aggregateSyncResponse(aggregator: SyncResponsePostTreatmentAggregator) {
+        relevantPlugins.measureSpan("task", "aggregator_management") {
+            // Everything else we need to do outside the transaction
+            measureTimeMillis {
+                aggregatorHandler.handle(aggregator)
+            }.also {
+                Timber.v("Aggregator management took $it ms")
+            }
         }
+    }
 
-        measureTimeMillis {
-            syncResponse.rooms?.let {
-                checkPushRules(it, isInitialSync)
-                userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
-                dispatchInvitedRoom(it)
+    private suspend fun postTreatmentSyncResponse(syncResponse: SyncResponse, isInitialSync: Boolean) {
+        relevantPlugins.measureSpan("task", "sync_response_post_treatment") {
+            measureTimeMillis {
+                syncResponse.rooms?.let {
+                    checkPushRules(it, isInitialSync)
+                    userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
+                    dispatchInvitedRoom(it)
+                }
+            }.also {
+                Timber.v("SyncResponse.rooms post treatment took $it ms")
             }
-        }.also {
-            Timber.v("SyncResponse.rooms post treatment took $it ms")
         }
+    }
 
-        measureTimeMillis {
-            cryptoSyncHandler.onSyncCompleted(syncResponse)
-        }.also {
-            Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms")
+    private fun markCryptoSyncCompleted(syncResponse: SyncResponse) {
+        relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") {
+            measureTimeMillis {
+                cryptoSyncHandler.onSyncCompleted(syncResponse)
+            }.also {
+                Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms")
+            }
         }
+    }
 
-        // post sync stuffs
+    private fun handlePostSync() {
         monarchy.writeAsync {
             roomSyncHandler.postSyncSpaceHierarchyHandle(it)
         }
-        Timber.v("On sync completed")
     }
 
     private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) {
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
new file mode 100644
index 0000000000000000000000000000000000000000..b30428e5e101c02a10ae7805ffba9a7776c3c8e5
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactoryTests.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.send
+
+import org.amshove.kluent.internal.assertEquals
+import org.junit.Before
+import org.junit.Test
+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
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.util.TextContent
+import org.matrix.android.sdk.test.fakes.FakeClock
+import org.matrix.android.sdk.test.fakes.FakeContext
+import org.matrix.android.sdk.test.fakes.internal.session.content.FakeThumbnailExtractor
+import org.matrix.android.sdk.test.fakes.internal.session.permalinks.FakePermalinkFactory
+import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeLocalEchoRepository
+import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeMarkdownParser
+import org.matrix.android.sdk.test.fakes.internal.session.room.send.FakeWaveFormSanitizer
+import org.matrix.android.sdk.test.fakes.internal.session.room.send.pills.FakeTextPillsUtils
+
+@Suppress("MaxLineLength")
+class LocalEchoEventFactoryTests {
+
+    companion object {
+        internal const val A_USER_ID_1 = "@user_1:matrix.org"
+        internal const val A_ROOM_ID = "!sUeOGZKsBValPTUMax:matrix.org"
+        internal const val AN_EVENT_ID = "\$vApgexcL8Vfh-WxYKsFKCDooo67ttbjm3TiVKXaWijU"
+        internal const val AN_EPOCH = 1655210176L
+
+        val A_START_EVENT = Event(
+                type = EventType.STATE_ROOM_CREATE,
+                eventId = AN_EVENT_ID,
+                originServerTs = 1652435922563,
+                senderId = A_USER_ID_1,
+                roomId = A_ROOM_ID
+        )
+    }
+
+    private val fakeContext = FakeContext()
+    private val fakeMarkdownParser = FakeMarkdownParser()
+    private val fakeTextPillsUtils = FakeTextPillsUtils()
+    private val fakeThumbnailExtractor = FakeThumbnailExtractor()
+    private val fakeWaveFormSanitizer = FakeWaveFormSanitizer()
+    private val fakeLocalEchoRepository = FakeLocalEchoRepository()
+    private val fakePermalinkFactory = FakePermalinkFactory()
+    private val fakeClock = FakeClock()
+
+    private val localEchoEventFactory = LocalEchoEventFactory(
+            context = fakeContext.instance,
+            userId = A_USER_ID_1,
+            markdownParser = fakeMarkdownParser.instance,
+            textPillsUtils = fakeTextPillsUtils.instance,
+            thumbnailExtractor = fakeThumbnailExtractor.instance,
+            waveformSanitizer = fakeWaveFormSanitizer.instance,
+            localEchoRepository = fakeLocalEchoRepository.instance,
+            permalinkFactory = fakePermalinkFactory.instance,
+            clock = fakeClock
+    )
+
+    @Before
+    fun setup() {
+        fakeClock.givenEpoch(AN_EPOCH)
+        fakeMarkdownParser.givenBoldMarkdown()
+    }
+
+    @Test
+    fun `given a null quotedText, when a quote event is created, then the result message should only contain the new text after new lines`() {
+        val event = createTimelineEvent(null, null)
+        val quotedContent = localEchoEventFactory.createQuotedTextEvent(
+                roomId = A_ROOM_ID,
+                quotedEvent = event,
+                text = "Text",
+                formattedText = null,
+                autoMarkdown = false,
+                rootThreadEventId = null,
+                additionalContent = null,
+        ).content.toModel<MessageContent>()
+        assertEquals("\n\nText", quotedContent?.body)
+        assertEquals("<br/>Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody)
+    }
+
+    @Test
+    fun `given a plain text quoted message, when a quote event is created, then the result message should contain both the quoted and new text`() {
+        val event = createTimelineEvent("Quoted", null)
+        val quotedContent = localEchoEventFactory.createQuotedTextEvent(
+                roomId = A_ROOM_ID,
+                quotedEvent = event,
+                text = "Text",
+                formattedText = null,
+                autoMarkdown = false,
+                rootThreadEventId = null,
+                additionalContent = null,
+        ).content.toModel<MessageContent>()
+        assertEquals("> Quoted\n\nText", quotedContent?.body)
+        assertEquals("<blockquote>Quoted</blockquote><br/>Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody)
+    }
+
+    @Test
+    fun `given a formatted text quoted message, when a quote event is created, then the result message should contain both the formatted quote and new text`() {
+        val event = createTimelineEvent("Quoted", "<b>Quoted</b>")
+        val quotedContent = localEchoEventFactory.createQuotedTextEvent(
+                roomId = A_ROOM_ID,
+                quotedEvent = event,
+                text = "Text",
+                formattedText = null,
+                autoMarkdown = false,
+                rootThreadEventId = null,
+                additionalContent = null,
+        ).content.toModel<MessageContent>()
+        // This still uses the plain text version
+        assertEquals("> Quoted\n\nText", quotedContent?.body)
+        // This one has the formatted one
+        assertEquals("<blockquote><b>Quoted</b></blockquote><br/>Text", (quotedContent as? MessageContentWithFormattedBody)?.formattedBody)
+    }
+
+    @Test
+    fun `given formatted text quoted message and new message, when a quote event is created, then the result message should contain both the formatted quote and new formatted text`() {
+        val event = createTimelineEvent("Quoted", "<b>Quoted</b>")
+        val quotedContent = localEchoEventFactory.createQuotedTextEvent(
+                roomId = A_ROOM_ID,
+                quotedEvent = event,
+                text = "Text",
+                formattedText = "<b>Formatted text</b>",
+                autoMarkdown = false,
+                rootThreadEventId = null,
+                additionalContent = null,
+        ).content.toModel<MessageContent>()
+        // This still uses the plain text version
+        assertEquals("> Quoted\n\nText", quotedContent?.body)
+        // This one has the formatted one
+        assertEquals(
+                "<blockquote><b>Quoted</b></blockquote><br/><b>Formatted text</b>",
+                (quotedContent as? MessageContentWithFormattedBody)?.formattedBody
+        )
+    }
+
+    @Test
+    fun `given formatted text quoted message and new message with autoMarkdown, when a quote event is created, then the result message should contain both the formatted quote and new formatted text, not the markdown processed text`() {
+        val event = createTimelineEvent("Quoted", "<b>Quoted</b>")
+        val quotedContent = localEchoEventFactory.createQuotedTextEvent(
+                roomId = A_ROOM_ID,
+                quotedEvent = event,
+                text = "Text",
+                formattedText = "<b>Formatted text</b>",
+                autoMarkdown = true,
+                rootThreadEventId = null,
+                additionalContent = null,
+        ).content.toModel<MessageContent>()
+        // This still uses the plain text version
+        assertEquals("> Quoted\n\nText", quotedContent?.body)
+        // This one has the formatted one
+        assertEquals(
+                "<blockquote><b>Quoted</b></blockquote><br/><b>Formatted text</b>",
+                (quotedContent as? MessageContentWithFormattedBody)?.formattedBody
+        )
+    }
+
+    @Test
+    fun `given a formatted text quoted message and a new message with autoMarkdown, when a quote event is created, then the result message should contain both the formatted quote and new processed formatted text`() {
+        val event = createTimelineEvent("Quoted", "<b>Quoted</b>")
+        val quotedContent = localEchoEventFactory.createQuotedTextEvent(
+                roomId = A_ROOM_ID,
+                quotedEvent = event,
+                text = "**Text**",
+                formattedText = null,
+                autoMarkdown = true,
+                rootThreadEventId = null,
+                additionalContent = null,
+        ).content.toModel<MessageContent>()
+        // This still uses the markdown text version
+        assertEquals("> Quoted\n\n**Text**", quotedContent?.body)
+        // This one has the formatted one
+        assertEquals(
+                "<blockquote><b>Quoted</b></blockquote><br/><b>Text</b>",
+                (quotedContent as? MessageContentWithFormattedBody)?.formattedBody
+        )
+    }
+
+    @Test
+    fun `given a plain text quoted message and a new message with autoMarkdown, when a quote event is created, then the result message should the plain text quote and new processed formatted text`() {
+        val event = createTimelineEvent("Quoted", null)
+        val quotedContent = localEchoEventFactory.createQuotedTextEvent(
+                roomId = A_ROOM_ID,
+                quotedEvent = event,
+                text = "**Text**",
+                formattedText = null,
+                autoMarkdown = true,
+                rootThreadEventId = null,
+                additionalContent = null,
+        ).content.toModel<MessageContent>()
+        // This still uses the markdown text version
+        assertEquals("> Quoted\n\n**Text**", quotedContent?.body)
+        // This one has the formatted one
+        assertEquals(
+                "<blockquote>Quoted</blockquote><br/><b>Text</b>",
+                (quotedContent as? MessageContentWithFormattedBody)?.formattedBody
+        )
+    }
+
+    private fun createTimelineEvent(quotedText: String?, formattedQuotedText: String?): TimelineEvent {
+        val textContent = quotedText?.let {
+            TextContent(
+                    quotedText,
+                    formattedQuotedText
+            ).toMessageTextContent().toContent()
+        }
+        return TimelineEvent(
+                root = A_START_EVENT,
+                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/test/fakes/FakeClipboardManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bce8b41aa9b334b1a00164284c5636e896488932
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClipboardManager.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.runs
+import io.mockk.verify
+
+class FakeClipboardManager {
+    val instance = mockk<ClipboardManager>()
+
+    fun givenSetPrimaryClip() {
+        every { instance.setPrimaryClip(any()) } just runs
+    }
+
+    fun verifySetPrimaryClip(clipData: ClipData) {
+        verify { instance.setPrimaryClip(clipData) }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5c3a245c517b7ef36302c7cac7f169c905d1615e
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeConnectivityManager.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes
+
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import io.mockk.every
+import io.mockk.mockk
+
+class FakeConnectivityManager {
+    val instance = mockk<ConnectivityManager>()
+
+    fun givenNoActiveConnection() {
+        every { instance.activeNetwork } returns null
+    }
+
+    fun givenHasActiveConnection() {
+        val network = mockk<Network>()
+        every { instance.activeNetwork } returns network
+
+        val networkCapabilities = FakeNetworkCapabilities()
+        networkCapabilities.givenTransports(
+                NetworkCapabilities.TRANSPORT_CELLULAR,
+                NetworkCapabilities.TRANSPORT_WIFI,
+                NetworkCapabilities.TRANSPORT_VPN
+        )
+        every { instance.getNetworkCapabilities(network) } returns networkCapabilities.instance
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt
new file mode 100644
index 0000000000000000000000000000000000000000..966c6a1bb20f3b5c225063768f8a0a09d01156cd
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeContext.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.test.fakes
+
+import android.content.ClipboardManager
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.net.ConnectivityManager
+import android.net.Uri
+import android.os.ParcelFileDescriptor
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.runs
+import java.io.OutputStream
+
+class FakeContext(
+        private val contentResolver: ContentResolver = mockk()
+) {
+
+    val instance = mockk<Context>()
+
+    init {
+        every { instance.contentResolver } returns contentResolver
+        every { instance.applicationContext } returns instance
+    }
+
+    fun givenFileDescriptor(uri: Uri, mode: String, factory: () -> ParcelFileDescriptor?) {
+        val fileDescriptor = factory()
+        every { contentResolver.openFileDescriptor(uri, mode, null) } returns fileDescriptor
+    }
+
+    fun givenSafeOutputStreamFor(uri: Uri): OutputStream {
+        val outputStream = mockk<OutputStream>(relaxed = true)
+        every { contentResolver.openOutputStream(uri, "wt") } returns outputStream
+        return outputStream
+    }
+
+    fun givenMissingSafeOutputStreamFor(uri: Uri) {
+        every { contentResolver.openOutputStream(uri, "wt") } returns null
+    }
+
+    fun givenNoConnection() {
+        val connectivityManager = FakeConnectivityManager()
+        connectivityManager.givenNoActiveConnection()
+        givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance)
+    }
+
+    fun <T> givenService(name: String, klass: Class<T>, service: T) {
+        every { instance.getSystemService(name) } returns service
+        every { instance.getSystemService(klass) } returns service
+    }
+
+    fun givenHasConnection() {
+        val connectivityManager = FakeConnectivityManager()
+        connectivityManager.givenHasActiveConnection()
+        givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance)
+    }
+
+    fun givenStartActivity(intent: Intent) {
+        every { instance.startActivity(intent) } just runs
+    }
+
+    fun givenClipboardManager(): FakeClipboardManager {
+        val fakeClipboardManager = FakeClipboardManager()
+        givenService(Context.CLIPBOARD_SERVICE, ClipboardManager::class.java, fakeClipboardManager.instance)
+        return fakeClipboardManager
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c630b94d474a869e1f6c7ee1f0f0a7abf1c6056e
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeNetworkCapabilities.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes
+
+import android.net.NetworkCapabilities
+import io.mockk.every
+import io.mockk.mockk
+
+class FakeNetworkCapabilities {
+    val instance = mockk<NetworkCapabilities>()
+
+    fun givenTransports(vararg type: Int) {
+        every { instance.hasTransport(any()) } answers {
+            val input = it.invocation.args.first() as Int
+            type.contains(input)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b541d24161713dc68413808f54779228a95f7935
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/content/FakeThumbnailExtractor.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.test.fakes.internal.session.content
+
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor
+
+class FakeThumbnailExtractor {
+    internal val instance = mockk<ThumbnailExtractor>()
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3d7e85424e826a2ecd0435b9577c74b3d9400a9c
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/permalinks/FakePermalinkFactory.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.test.fakes.internal.session.permalinks
+
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
+
+class FakePermalinkFactory {
+    internal val instance = mockk<PermalinkFactory>()
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b10d13824bdea85bfa06e673a35acc1e426f0252
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeLocalEchoRepository.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.test.fakes.internal.session.room.send
+
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
+
+class FakeLocalEchoRepository {
+    internal val instance = mockk<LocalEchoRepository>()
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a27c9284e7c2fa56050ca0ecf523de4674069afc
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeMarkdownParser.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes.internal.session.room.send
+
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.api.util.TextContent
+import org.matrix.android.sdk.internal.session.room.send.MarkdownParser
+
+class FakeMarkdownParser {
+    internal val instance = mockk<MarkdownParser>()
+    fun givenBoldMarkdown() {
+        every { instance.parse(any(), any(), any()) } answers {
+            val text = arg<String>(0)
+            TextContent(text, "<b>${text.replace("*", "")}</b>")
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..052ddf78316ad93ae45d847dd45a2462c5590303
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/FakeWaveFormSanitizer.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.test.fakes.internal.session.room.send
+
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.room.send.WaveFormSanitizer
+
+class FakeWaveFormSanitizer {
+    internal val instance = mockk<WaveFormSanitizer>()
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0d783d662848619845b857e9bf090f0d34a09555
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/send/pills/FakeTextPillsUtils.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.test.fakes.internal.session.room.send.pills
+
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
+
+class FakeTextPillsUtils {
+    internal val instance = mockk<TextPillsUtils>()
+}
diff --git a/tools/import_from_element.sh b/tools/import_from_element.sh
index 3a7c221a3c4031b836734656cad4bc7ef7f6eb60..cebff08b95b71bb5cf131c3f32be149fe9824420 100755
--- a/tools/import_from_element.sh
+++ b/tools/import_from_element.sh
@@ -2,6 +2,9 @@
 
 ### This script import SDK code from Element Android
 
+printf "Please run ./tools/releaseScript.sh now!\n"
+exit 1
+
 set -e
 
 elementAndroidPath="../element-android"
diff --git a/tools/releaseScript.sh b/tools/releaseScript.sh
new file mode 100755
index 0000000000000000000000000000000000000000..087e583c2b03d1dc0faaaf4ba0d784b0464eaa74
--- /dev/null
+++ b/tools/releaseScript.sh
@@ -0,0 +1,200 @@
+#!/usr/bin/env bash
+
+#
+# Copyright (c) 2022 New Vector Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Ignore any error to not stop the script
+set +e
+
+printf "\n"
+printf "================================================================================\n"
+printf "|                    Welcome to the release script!                            |\n"
+printf "================================================================================\n"
+
+# Guessing version to propose a default version
+printf "\nGuessing the version...\n"
+# Get the version from the latest main from Element
+# curl -s  https://raw.githubusercontent.com/vector-im/element-android/main/matrix-sdk-android/build.gradle |grep "SDK_VERSION" |cut -d "\"" -f7|cut -d "\\" -f1
+versionCandidate=`curl -s https://raw.githubusercontent.com/vector-im/element-android/main/matrix-sdk-android/build.gradle | grep SDK_VERSION | cut -d "\"" -f7 | cut -d "\\\\" -f1`
+
+printf "\n"
+read -p "Please enter the release version (example: ${versionCandidate}). Just press enter if ${versionCandidate} is correct. " version
+version=${version:-${versionCandidate}}
+
+printf "\n================================================================================\n"
+printf "Ensuring main and develop branches are up to date...\n"
+
+git checkout main
+git pull
+git checkout develop
+git pull
+
+printf "\n================================================================================\n"
+printf "Starting the release ${version}\n"
+git flow release start ${version}
+
+# Note: in case the release is already started and the script is started again, checkout the release branch again.
+ret=$?
+if [[ $ret -ne 0 ]]; then
+  printf "Mmh, it seems that the release is already started. Checking out the release branch...\n"
+  git checkout "release/${version}"
+fi
+
+printf "\n================================================================================\n"
+printf "Updating version to ${version}\n"
+
+cp ./gradle.properties ./gradle.properties.bak
+sed "s/VERSION_NAME=.*$/VERSION_NAME=${version}/" ./gradle.properties.bak > ./gradle.properties
+rm ./gradle.properties.bak
+git commit -a -m "Setting version for the release ${version}"
+
+printf "\n================================================================================\n"
+printf "Downloading Element Android source ${version}...\n"
+
+curl  https://github.com/vector-im/element-android/archive/refs/tags/v${version}.zip -i -L -o ./tmp/release.zip
+
+printf "\n================================================================================\n"
+printf "Unzipping Element Android source ${version}...\n"
+
+# Delete existing path if any
+if [[ -e "./tmp/element-android-${version}" ]]; then
+  rm -rf ./tmp/element-android-${version}
+fi
+
+unzip -q ./tmp/release.zip -d ./tmp/
+
+printf "\n================================================================================\n"
+printf "Importing the module matrix-sdk-android...\n"
+elementAndroidPath="./tmp/element-android-${version}"
+# Delete existing path
+rm -rf ./matrix-sdk-android
+cp -r ${elementAndroidPath}/matrix-sdk-android .
+
+# Copy other files
+cp -r ${elementAndroidPath}/gradle/wrapper/gradle-wrapper.properties ./gradle/wrapper/gradle-wrapper.properties
+cp -r ${elementAndroidPath}/dependencies.gradle .
+cp -r ${elementAndroidPath}/dependencies_groups.gradle .
+
+# Add all changes to git
+git add -A
+
+## Check the number of diff in the file ./matrix-sdk-android/build.gradle. If 2 we can revert it, else it will have to be done manually
+nbDiff=`git diff --staged ./matrix-sdk-android/build.gradle | grep "@@" | wc -l | cut -d " " -f8`
+if [[ ${nbDiff} -ne 2 ]]; then
+  printf "\n================================================================================\n"
+  read -p "Cannot reset file ./matrix-sdk-android/build.gradle automatically. Please check the diff, and restore some part. Press enter when it's done "
+else
+  git reset ./matrix-sdk-android/build.gradle
+  git restore ./matrix-sdk-android/build.gradle
+fi
+
+# Add all changes to git
+git add -A
+
+printf "\n================================================================================\n"
+printf "Building the library.\n"
+
+# Do not ignore errors
+set -e
+# Build the library
+./gradlew clean assembleRelease --stacktrace
+set +e
+
+printf "\n================================================================================\n"
+read -p "OK, I will commit the changes, press Enter to continue "
+git commit -a -m "Import v${version} from Element Android"
+
+printf "\n================================================================================\n"
+printf "Preparing CHANGES.md file. You can copy this block at the top of the file, and import the part about SDK change from Element Android. Do not commit.\n\n"
+
+date=`date +%Y-%m-%d`
+printf "Changes in Matrix-SDK v${version} (${date})\n"
+printf "=========================================\n"
+printf "\n"
+printf "Imported from Element ${version}. (https://github.com/vector-im/element-android/releases/tag/v${version})\n"
+printf "\n"
+printf "SDK API changes ⚠️\n"
+# TIL
+printf -- "------------------\n"
+printf "TODO COPY PASTE BLOCK FROM ELEMENT AT https://raw.githubusercontent.com/vector-im/element-android/main/CHANGES.md\n"
+printf "\n"
+printf "\n"
+
+read -p "Press Enter when it's done, I will do the commit. "
+
+printf "Committing...\n"
+git commit -a -m "Changelog for version ${version}"
+
+printf "\n================================================================================\n"
+printf "OK, finishing the release...\n"
+git flow release finish "${version}"
+
+printf "\n================================================================================\n"
+read -p "Done, push the branch 'main' and the new tag (yes/no) default to yes? " doPush
+doPush=${doPush:-yes}
+
+if [ ${doPush} == "yes" ]; then
+  printf "Pushing branch 'main' and tag 'v${version}'...\n"
+  git push origin main
+  git push origin "v${version}"
+else
+    printf "Not pushing, do not forget to push manually!\n"
+fi
+
+printf "\n================================================================================\n"
+printf "Checking out develop...\n"
+git checkout develop
+
+printf "\n================================================================================\n"
+read -p "Done, push the branch 'develop' (yes/no) default to yes? (A rebase may be necessary in case develop got new commits)" doPush
+doPush=${doPush:-yes}
+
+if [ ${doPush} == "yes" ]; then
+  printf "Pushing branch 'develop'...\n"
+  git push origin develop
+else
+    printf "Not pushing, do not forget to push manually!\n"
+fi
+
+printf "\n================================================================================\n"
+printf "Going back to main branch to release the library...\n"
+git checkout main
+
+printf "\n================================================================================\n"
+printf "Building and releasing the library...\n"
+./gradlew clean publish --no-daemon --no-parallel
+./gradlew closeAndReleaseRepository
+
+printf "\n================================================================================\n"
+printf "Downloading the aar from MavenCentral (from https://repo1.maven.org/maven2/org/matrix/android/matrix-android-sdk2/)...\n"
+
+aarFile="matrix-android-sdk2-${version}.aar"
+command="curl --fail https://repo1.maven.org/maven2/org/matrix/android/matrix-android-sdk2/${version}/${aarFile} -s -i -L -o ./tmp/${aarFile}"
+${command}
+while [[ $? -ne 0 ]]
+do
+  printf "Not available yet, waiting 30s and retrying...\n"
+  sleep 30
+  ${command}
+done
+
+printf "\n================================================================================\n"
+read -p "Create the release on gitHub from the tag https://github.com/matrix-org/matrix-android-sdk2/tags, copy paste the block from the file CHANGES.md. Press enter when it's done."
+read -p "Add the aar ${aarFile} to the GitHub release. It has been downloaded in the folder ./tmp. Press enter when it's done."
+
+printf "\n================================================================================\n"
+printf "Congratulation! Kudos for using this script! Have a nice day!\n"
+printf "================================================================================\n"