diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ThreadToReplyMapInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ThreadToReplyMapInterceptor.kt
new file mode 100644
index 0000000000..519e497e95
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ThreadToReplyMapInterceptor.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.network
+
+import okhttp3.Interceptor
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.ResponseBody
+import okhttp3.ResponseBody.Companion.toResponseBody
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.network.ApiInterceptorListener
+import org.matrix.android.sdk.api.network.ApiPath
+import org.matrix.android.sdk.internal.di.MatrixScope
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * The goal of this interceptor is to map thread events to be handled as replies.
+ * The interceptor is responsible for mapping a thread event:
+ *          "m.relates_to":{
+ *              "event_id":"$eventId",
+ *              "rel_type":"io.element.thread"
+ *          }
+ * to an equivalent reply event:
+ *           m.relates_to":{
+ *              "m.in_reply_to":{
+ *              "event_id":"$eventId"
+ *           }
+ */
+@MatrixScope
+internal class ThreadToReplyMapInterceptor @Inject constructor() : Interceptor {
+
+    init {
+        Timber.d("MapThreadToReplyInterceptor.init")
+    }
+
+    override fun intercept(chain: Interceptor.Chain): Response {
+        val request = chain.request()
+
+        val response = chain.proceed(request)
+
+        if (isSyncRequest(request)) {
+            Timber.i(" ------> found SYNC REQUEST")
+
+            return response.body?.let {
+                val contentType = it.contentType()
+                val rawBody = it.string()
+                Timber.i(" ------> $rawBody")
+
+                if(rawBody.contains("\"rel_type\":\"io.element.thread\"")){
+                    Timber.i(" ------> Thread found")
+                    val start = rawBody.indexOf("\"rel_type\":\"io.element.thread\"") - "\"m.relates_to\":{\"event_id\":\"-GoMTnxkfmZczOPvbjcK43WqNib3wiJVaeO_vRxwHIDA\",\"".length +1
+                    val end = rawBody.indexOf("\"rel_type\":\"io.element.thread\"") + "\"rel_type\":\"io.element.thread\"".length +2
+                    val substr = rawBody.subSequence(start,end)
+                    val newRaw = rawBody.replaceRange(start,end,"\"m.relates_to\":{\"m.in_reply_to\":{\"event_id\":\"\$HDddlX2bJQmVS0bN5R9HDzcrGDap18b3cFDDYjTjctc\"}},")
+                    Timber.i(" ------> ${substr}")
+                    Timber.i(" ------> new raw $newRaw")
+                    val newBody = newRaw.toResponseBody(contentType)
+                    response.newBuilder().body(newBody).build()
+
+                }else{
+                    val newBody = rawBody.toResponseBody(contentType)
+                    response.newBuilder().body(newBody).build()
+                }
+            } ?: response
+
+//            response.peekBody(Long.MAX_VALUE).string().let { networkResponse ->
+//                Timber.i(" ------> ThreadToReplyMapInterceptor $networkResponse")
+//            }
+        }
+
+//        val path = request.url.encodedPath
+//        if(path.contains("/sync/")){
+//            Timber.i("-----> SYNC REQUEST --> $responseBody")
+//
+//
+//        }
+
+//        val body = ResponseBody.create()
+//        val newResponse = response.newBuilder().body(body)
+        return response
+    }
+
+    /**
+     * Returns true if the request is a sync request, false otherwise
+     * Example of a sync request:
+     * https://matrix-client.matrix.org/_matrix/client/r0/sync?filter=0&set_presence=online&t...
+     */
+    private fun isSyncRequest(request: Request): Boolean =
+            ApiPath.SYNC.method == request.method && request.url.encodedPath.contains(ApiPath.SYNC.path)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
index 8c4af81c99..db4e07a21a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
@@ -76,6 +76,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val cryptoService: DefaultCryptoService,
                                                    private val roomMemberEventHandler: RoomMemberEventHandler,
                                                    private val roomTypingUsersHandler: RoomTypingUsersHandler,
+                                                   private val threadsAwarenessHandler: ThreadsAwarenessHandler,
                                                    private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
                                                    @UserId private val userId: String,
                                                    private val timelineInput: TimelineInput) {
@@ -366,6 +367,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 decryptIfNeeded(event, roomId)
             }
 
+            threadsAwarenessHandler.handleIfNeeded(realm, roomId, event, ::decryptIfNeeded)
+
             val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
             val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
             if (event.stateKey != null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
new file mode 100644
index 0000000000..cc01f62448
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.sync.handler.room
+
+import io.realm.Realm
+import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
+import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
+import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent
+import org.matrix.android.sdk.api.util.JsonDict
+import org.matrix.android.sdk.internal.database.mapper.EventMapper
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.EventInsertType
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.RoomTagEntity
+import org.matrix.android.sdk.internal.database.query.getOrCreate
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * This handler is responsible for a smooth threads migration. It will map all incoming
+ * threads as replies. So a device without threads enabled/updated will be able to view
+ * threads response as replies to the orighinal message
+ */
+internal class ThreadsAwarenessHandler @Inject constructor(
+        private val permalinkFactory: PermalinkFactory
+) {
+
+    fun handleIfNeeded(realm: Realm,
+                       roomId: String,
+                       event: Event,
+                       decryptIfNeeded: (event: Event, roomId: String) -> Unit) {
+
+        if (!isThreadEvent(event)) return
+        val rootThreadEventId = getRootThreadEventId(event) ?: return
+        val payload = event.mxDecryptionResult?.payload?.toMutableMap() ?: return
+        val body = getValueFromPayload(payload, "body") ?: return
+        val msgType = getValueFromPayload(payload, "msgtype") ?: return
+        val rootThreadEventEntity = EventEntity.where(realm, eventId = rootThreadEventId).findFirst() ?: return
+        val rootThreadEvent = EventMapper.map(rootThreadEventEntity)
+        val rootThreadEventSenderId = rootThreadEvent.senderId ?: return
+        val rootThreadEventEventId = rootThreadEvent.eventId ?: return
+
+        Timber.i("------> Thread event detected!")
+
+        if (rootThreadEvent.isEncrypted()) {
+            decryptIfNeeded(rootThreadEvent, roomId)
+        }
+
+        val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(),"body")
+
+        val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventEventId, false)
+        val userLink =  permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: ""
+
+        val replyFormatted = LocalEchoEventFactory.REPLY_PATTERN.format(
+                permalink,
+                userLink,
+                rootThreadEventSenderId,
+                // Remove inner mx_reply tags if any
+                rootThreadEventBody,
+                body)
+
+        val messageTextContent =  MessageTextContent(
+                msgType = msgType,
+                format = MessageFormat.FORMAT_MATRIX_HTML,
+                body = body,
+                formattedBody = replyFormatted
+        ).toContent()
+
+        payload["content"] = messageTextContent
+
+        event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload )
+
+    }
+
+    private fun isThreadEvent(event: Event): Boolean =
+            event.content.toModel<MessageRelationContent>()?.relatesTo?.type == "io.element.thread"
+
+    private fun getRootThreadEventId(event: Event): String? =
+            event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
+
+    @Suppress("UNCHECKED_CAST")
+    private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
+        val content = payload?.get("content") as? JsonDict
+        return content?.get(key) as? String
+    }
+}