From 36866dd24e8c4ce3d5288baadcdea8d46a29e4d2 Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoit.marty@gmail.com>
Date: Thu, 12 Sep 2019 15:14:17 +0200
Subject: [PATCH] Save draft of a message when exiting a room with non empty
 composer (#329)

---
 CHANGES.md                                    |   2 +-
 .../main/java/im/vector/matrix/rx/RxRoom.kt   |   5 +
 .../matrix/android/api/session/room/Room.kt   |   2 +
 .../api/session/room/model/RoomSummary.kt     |   4 +-
 .../api/session/room/send/DraftService.kt     |  39 ++++
 .../api/session/room/send/UserDraft.kt        |  38 ++++
 .../internal/database/mapper/DraftMapper.kt   |  45 +++++
 .../database/mapper/RoomSummaryMapper.kt      |   5 +-
 .../internal/database/model/DraftEntity.kt    |  34 ++++
 .../database/model/RoomSummaryEntity.kt       |   3 +-
 .../database/model/SessionRealmModule.kt      |   4 +-
 .../database/model/UserDraftsEntity.kt        |  36 ++++
 .../database/query/UserDraftsEntityQueries.kt |  33 ++++
 .../internal/session/room/DefaultRoom.kt      |   3 +
 .../internal/session/room/RoomFactory.kt      |   3 +
 .../session/room/draft/DefaultDraftService.kt | 166 ++++++++++++++++
 .../session/room/send/DefaultSendService.kt   |  29 ++-
 vector/build.gradle                           |   5 +-
 .../home/room/detail/RoomDetailActions.kt     |  10 +-
 .../home/room/detail/RoomDetailFragment.kt    | 116 ++++++-----
 .../home/room/detail/RoomDetailViewModel.kt   | 187 ++++++++++++------
 .../home/room/detail/RoomDetailViewState.kt   |  12 +-
 .../home/room/list/RoomSummaryItem.kt         |   3 +
 .../home/room/list/RoomSummaryItemFactory.kt  |   1 +
 vector/src/main/res/layout/item_room.xml      |  22 ++-
 25 files changed, 667 insertions(+), 140 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserDraft.kt
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/DraftMapper.kt
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/DraftEntity.kt
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserDraftsEntity.kt
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UserDraftsEntityQueries.kt
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt

diff --git a/CHANGES.md b/CHANGES.md
index 75b11bdbd4..63ccaea83a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,7 +2,7 @@ Changes in RiotX 0.6.0 (2019-XX-XX)
 ===================================================
 
 Features:
- -
+ - Save draft of a message when exiting a room with non empty composer (#329)
 
 Improvements:
  - Add unread indent on room list (#485)
diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
index 0ff0987dfe..28a3d40070 100644
--- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
+++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt
@@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.room.Room
 import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
 import im.vector.matrix.android.api.session.room.model.ReadReceipt
 import im.vector.matrix.android.api.session.room.model.RoomSummary
+import im.vector.matrix.android.api.session.room.send.UserDraft
 import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 import io.reactivex.Observable
 import io.reactivex.Single
@@ -54,6 +55,10 @@ class RxRoom(private val room: Room) {
         return room.getEventReadReceiptsLive(eventId).asObservable()
     }
 
+    fun liveDrafts(): Observable<List<UserDraft>> {
+        return room.getDraftsLive().asObservable()
+    }
+
 }
 
 fun Room.rx(): RxRoom {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt
index ec6b382f8f..92414eb768 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt
@@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.room.members.MembershipService
 import im.vector.matrix.android.api.session.room.model.RoomSummary
 import im.vector.matrix.android.api.session.room.model.relation.RelationService
 import im.vector.matrix.android.api.session.room.read.ReadService
+import im.vector.matrix.android.api.session.room.send.DraftService
 import im.vector.matrix.android.api.session.room.send.SendService
 import im.vector.matrix.android.api.session.room.state.StateService
 import im.vector.matrix.android.api.session.room.timeline.TimelineService
@@ -32,6 +33,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService
 interface Room :
         TimelineService,
         SendService,
+        DraftService,
         ReadService,
         MembershipService,
         StateService,
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt
index e4bf1bd32b..099deae937 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt
@@ -17,6 +17,7 @@
 package im.vector.matrix.android.api.session.room.model
 
 import im.vector.matrix.android.api.session.room.model.tag.RoomTag
+import im.vector.matrix.android.api.session.room.send.UserDraft
 import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 
 /**
@@ -36,7 +37,8 @@ data class RoomSummary(
         val hasUnreadMessages: Boolean = false,
         val tags: List<RoomTag> = emptyList(),
         val membership: Membership = Membership.NONE,
-        val versioningState: VersioningState = VersioningState.NONE
+        val versioningState: VersioningState = VersioningState.NONE,
+        val userDrafts: List<UserDraft> = emptyList()
 ) {
 
     val isVersioned: Boolean
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt
new file mode 100644
index 0000000000..c700b40a08
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/DraftService.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019 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.
+ */
+
+package im.vector.matrix.android.api.session.room.send
+
+import androidx.lifecycle.LiveData
+
+interface DraftService {
+
+    /**
+     * Save or update a draft to the room
+     */
+    fun saveDraft(draft: UserDraft)
+
+    /**
+     * Delete the last draft, basically just after sending the message
+     */
+    fun deleteDraft()
+
+    /**
+     * Return the current drafts if any, as a live data
+     * The draft list can contain one draft for {regular, reply, quote} and an arbitrary number of {edit} drafts
+     */
+    fun getDraftsLive(): LiveData<List<UserDraft>>
+
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserDraft.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserDraft.kt
new file mode 100644
index 0000000000..8912cc2580
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserDraft.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 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.
+ */
+
+package im.vector.matrix.android.api.session.room.send
+
+/**
+ * Describes a user draft:
+ * REGULAR: draft of a classical message
+ * QUOTE: draft of a message which quotes another message
+ * EDIT: draft of an edition of a message
+ * REPLY: draft of a reply of another message
+ */
+sealed class UserDraft(open val text: String) {
+    data class REGULAR(override val text: String) : UserDraft(text)
+    data class QUOTE(val linkedEventId: String, override val text: String) : UserDraft(text)
+    data class EDIT(val linkedEventId: String, override val text: String) : UserDraft(text)
+    data class REPLY(val linkedEventId: String, override val text: String) : UserDraft(text)
+
+    fun isValid(): Boolean {
+        return when (this) {
+            is REGULAR -> text.isNotBlank()
+            else       -> true
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/DraftMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/DraftMapper.kt
new file mode 100644
index 0000000000..6b87951e0a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/DraftMapper.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 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.
+ */
+
+package im.vector.matrix.android.internal.database.mapper
+
+import im.vector.matrix.android.api.session.room.send.UserDraft
+import im.vector.matrix.android.internal.database.model.DraftEntity
+
+/**
+ * DraftEntity <-> UserDraft
+ */
+internal object DraftMapper {
+
+    fun map(entity: DraftEntity): UserDraft {
+        return when (entity.draftMode) {
+            DraftEntity.MODE_REGULAR -> UserDraft.REGULAR(entity.content)
+            DraftEntity.MODE_EDIT    -> UserDraft.EDIT(entity.linkedEventId, entity.content)
+            DraftEntity.MODE_QUOTE   -> UserDraft.QUOTE(entity.linkedEventId, entity.content)
+            DraftEntity.MODE_REPLY   -> UserDraft.REPLY(entity.linkedEventId, entity.content)
+            else                     -> null
+        } ?: UserDraft.REGULAR("")
+    }
+
+    fun map(domain: UserDraft): DraftEntity {
+        return when (domain) {
+            is UserDraft.REGULAR -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "")
+            is UserDraft.EDIT    -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId)
+            is UserDraft.QUOTE   -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId)
+            is UserDraft.REPLY   -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId)
+        }
+    }
+}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt
index 8cb738807f..4fbe7fe04c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt
@@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
 import im.vector.matrix.android.api.session.room.model.tag.RoomTag
 import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
 import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
-import java.util.UUID
+import java.util.*
 import javax.inject.Inject
 
 internal class RoomSummaryMapper @Inject constructor(
@@ -67,7 +67,8 @@ internal class RoomSummaryMapper @Inject constructor(
                 hasUnreadMessages = roomSummaryEntity.hasUnreadMessages,
                 tags = tags,
                 membership = roomSummaryEntity.membership,
-                versioningState = roomSummaryEntity.versioningState
+                versioningState = roomSummaryEntity.versioningState,
+                userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList()
         )
     }
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/DraftEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/DraftEntity.kt
new file mode 100644
index 0000000000..9666ebd9a1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/DraftEntity.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 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.
+ */
+
+package im.vector.matrix.android.internal.database.model
+
+import io.realm.RealmObject
+
+internal open class DraftEntity(var content: String = "",
+                                var draftMode: String = MODE_REGULAR,
+                                var linkedEventId: String = ""
+
+) : RealmObject() {
+
+    companion object {
+        const val MODE_REGULAR = "REGULAR"
+        const val MODE_EDIT = "EDIT"
+        const val MODE_REPLY = "REPLY"
+        const val MODE_QUOTE = "QUOTE"
+    }
+}
+
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt
index 95308d367e..1c159b23d2 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt
@@ -36,7 +36,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
                                       var notificationCount: Int = 0,
                                       var highlightCount: Int = 0,
                                       var hasUnreadMessages: Boolean = false,
-                                      var tags: RealmList<RoomTagEntity> = RealmList()
+                                      var tags: RealmList<RoomTagEntity> = RealmList(),
+                                      var userDrafts: UserDraftsEntity? = null
 ) : RealmObject() {
 
     private var membershipStr: String = Membership.NONE.name
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt
index 1d27bf07ee..680e2eac7d 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt
@@ -43,6 +43,8 @@ import io.realm.annotations.RealmModule
                  PushConditionEntity::class,
                  PusherEntity::class,
                  PusherDataEntity::class,
-                 ReadReceiptsSummaryEntity::class
+                 ReadReceiptsSummaryEntity::class,
+                 UserDraftsEntity::class,
+                 DraftEntity::class
              ])
 internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserDraftsEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserDraftsEntity.kt
new file mode 100644
index 0000000000..b713fe1c3f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/UserDraftsEntity.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 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.
+ */
+
+package im.vector.matrix.android.internal.database.model
+
+import io.realm.RealmList
+import io.realm.RealmObject
+import io.realm.RealmResults
+import io.realm.annotations.LinkingObjects
+
+/**
+ * Create a specific table to be able to do direct query on it and keep the draft ordered
+ */
+internal open class UserDraftsEntity(var userDrafts: RealmList<DraftEntity> = RealmList()
+) : RealmObject() {
+
+    // Link to RoomSummaryEntity
+    @LinkingObjects("userDrafts")
+    val roomSummaryEntity: RealmResults<RoomSummaryEntity>? = null
+
+    companion object
+
+}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UserDraftsEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UserDraftsEntityQueries.kt
new file mode 100644
index 0000000000..ae368c5850
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/UserDraftsEntityQueries.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 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.
+ */
+
+package im.vector.matrix.android.internal.database.query
+
+import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
+import im.vector.matrix.android.internal.database.model.UserDraftsEntity
+import im.vector.matrix.android.internal.database.model.UserDraftsEntityFields
+import io.realm.Realm
+import io.realm.RealmQuery
+import io.realm.kotlin.where
+
+internal fun UserDraftsEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<UserDraftsEntity> {
+    val query = realm.where<UserDraftsEntity>()
+    if (roomId != null) {
+        query.equalTo(UserDraftsEntityFields.ROOM_SUMMARY_ENTITY + "." + RoomSummaryEntityFields.ROOM_ID, roomId)
+    }
+    return query
+}
+
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt
index 492dd03543..6b2a6843f1 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt
@@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.room.members.MembershipService
 import im.vector.matrix.android.api.session.room.model.RoomSummary
 import im.vector.matrix.android.api.session.room.model.relation.RelationService
 import im.vector.matrix.android.api.session.room.read.ReadService
+import im.vector.matrix.android.api.session.room.send.DraftService
 import im.vector.matrix.android.api.session.room.send.SendService
 import im.vector.matrix.android.api.session.room.state.StateService
 import im.vector.matrix.android.api.session.room.timeline.TimelineService
@@ -40,6 +41,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
                                                private val roomSummaryMapper: RoomSummaryMapper,
                                                private val timelineService: TimelineService,
                                                private val sendService: SendService,
+                                               private val draftService: DraftService,
                                                private val stateService: StateService,
                                                private val readService: ReadService,
                                                private val cryptoService: CryptoService,
@@ -48,6 +50,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
 ) : Room,
     TimelineService by timelineService,
     SendService by sendService,
+    DraftService by draftService,
     StateService by stateService,
     ReadService by readService,
     RelationService by relationService,
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt
index 53da2d7709..e972f6a98e 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt
@@ -20,6 +20,7 @@ import com.zhuinden.monarchy.Monarchy
 import im.vector.matrix.android.api.session.crypto.CryptoService
 import im.vector.matrix.android.api.session.room.Room
 import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
+import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
 import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
 import im.vector.matrix.android.internal.session.room.read.DefaultReadService
 import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService
@@ -38,6 +39,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
                                                       private val cryptoService: CryptoService,
                                                       private val timelineServiceFactory: DefaultTimelineService.Factory,
                                                       private val sendServiceFactory: DefaultSendService.Factory,
+                                                      private val draftServiceFactory: DefaultDraftService.Factory,
                                                       private val stateServiceFactory: DefaultStateService.Factory,
                                                       private val readServiceFactory: DefaultReadService.Factory,
                                                       private val relationServiceFactory: DefaultRelationService.Factory,
@@ -51,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
                 roomSummaryMapper,
                 timelineServiceFactory.create(roomId),
                 sendServiceFactory.create(roomId),
+                draftServiceFactory.create(roomId),
                 stateServiceFactory.create(roomId),
                 readServiceFactory.create(roomId),
                 cryptoService,
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt
new file mode 100644
index 0000000000..c5676f84c8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/draft/DefaultDraftService.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2019 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.
+ */
+
+package im.vector.matrix.android.internal.session.room.draft
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Transformations
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import com.zhuinden.monarchy.Monarchy
+import im.vector.matrix.android.BuildConfig
+import im.vector.matrix.android.api.session.room.send.DraftService
+import im.vector.matrix.android.api.session.room.send.UserDraft
+import im.vector.matrix.android.internal.database.RealmLiveData
+import im.vector.matrix.android.internal.database.mapper.DraftMapper
+import im.vector.matrix.android.internal.database.model.DraftEntity
+import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
+import im.vector.matrix.android.internal.database.model.UserDraftsEntity
+import im.vector.matrix.android.internal.database.query.where
+import io.realm.kotlin.createObject
+import timber.log.Timber
+
+internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String,
+                                                               private val monarchy: Monarchy
+) : DraftService {
+
+    @AssistedInject.Factory
+    interface Factory {
+        fun create(roomId: String): DraftService
+    }
+
+    /**
+     * The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft,
+     * or even move an existing draft to the top of the list
+     */
+    override fun saveDraft(draft: UserDraft) {
+        Timber.d("Draft: saveDraft ${privacySafe(draft)}")
+
+        monarchy.writeAsync { realm ->
+
+            val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
+
+            val userDraftsEntity = roomSummaryEntity.userDrafts
+                    ?: realm.createObject<UserDraftsEntity>().also {
+                        roomSummaryEntity.userDrafts = it
+                    }
+
+            userDraftsEntity.let { userDraftEntity ->
+                // Save only valid draft
+                if (draft.isValid()) {
+                    // Add a new draft or update the current one?
+                    val newDraft = DraftMapper.map(draft)
+
+                    // Is it an update of the top draft?
+                    val topDraft = userDraftEntity.userDrafts.lastOrNull()
+
+                    if (topDraft == null) {
+                        Timber.d("Draft: create a new draft ${privacySafe(draft)}")
+                        userDraftEntity.userDrafts.add(newDraft)
+                    } else if (topDraft.draftMode == DraftEntity.MODE_EDIT) {
+                        // top draft is an edit
+                        if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
+                            if (topDraft.linkedEventId == newDraft.linkedEventId) {
+                                // Update the top draft
+                                Timber.d("Draft: update the top edit draft ${privacySafe(draft)}")
+                                topDraft.content = newDraft.content
+                            } else {
+                                // Check a previously EDIT draft with the same id
+                                val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find {
+                                    it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId
+                                }
+
+                                if (existingEditDraftOfSameEvent != null) {
+                                    // Ignore the new text, restore what was typed before, by putting the draft to the top
+                                    Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}")
+                                    userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent)
+                                    userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent)
+                                } else {
+                                    Timber.d("Draft: add a new edit draft ${privacySafe(draft)}")
+                                    userDraftEntity.userDrafts.add(newDraft)
+                                }
+                            }
+                        } else {
+                            // Add a new regular draft to the top
+                            Timber.d("Draft: add a new draft ${privacySafe(draft)}")
+                            userDraftEntity.userDrafts.add(newDraft)
+                        }
+                    } else {
+                        // Top draft is not an edit
+                        if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
+                            Timber.d("Draft: create a new edit draft ${privacySafe(draft)}")
+                            userDraftEntity.userDrafts.add(newDraft)
+                        } else {
+                            // Update the top draft
+                            Timber.d("Draft: update the top draft ${privacySafe(draft)}")
+                            topDraft.draftMode = newDraft.draftMode
+                            topDraft.content = newDraft.content
+                            topDraft.linkedEventId = newDraft.linkedEventId
+                        }
+                    }
+                } else {
+                    // There is no draft to save, so the composer was clear
+                    Timber.d("Draft: delete a draft")
+
+                    val topDraft = userDraftEntity.userDrafts.lastOrNull()
+
+                    if (topDraft == null) {
+                        Timber.d("Draft: nothing to do")
+                    } else {
+                        // Remove the top draft
+                        Timber.d("Draft: remove the top draft")
+                        userDraftEntity.userDrafts.remove(topDraft)
+                    }
+                }
+            }
+        }
+    }
+
+    private fun privacySafe(o: Any): Any {
+        if (BuildConfig.LOG_PRIVATE_DATA) {
+            return o
+        }
+
+        return ""
+    }
+
+    override fun deleteDraft() {
+        Timber.d("Draft: deleteDraft()")
+
+        monarchy.writeAsync { realm ->
+            UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity ->
+                if (userDraftsEntity.userDrafts.isNotEmpty()) {
+                    userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1)
+                }
+            }
+        }
+    }
+
+    override fun getDraftsLive(): LiveData<List<UserDraft>> {
+        val liveData = RealmLiveData(monarchy.realmConfiguration) {
+            UserDraftsEntity.where(it, roomId)
+        }
+
+        return Transformations.map(liveData) { userDraftsEntities ->
+            userDraftsEntities.firstOrNull()?.let { userDraftEntity ->
+                userDraftEntity.userDrafts.map { draftEntity ->
+                    DraftMapper.map(draftEntity)
+                }
+            } ?: emptyList()
+        }
+    }
+}
+
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt
index 2c20839b26..a97d03d734 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt
@@ -17,33 +17,29 @@
 package im.vector.matrix.android.internal.session.room.send
 
 import android.content.Context
-import androidx.work.BackoffPolicy
-import androidx.work.ExistingWorkPolicy
-import androidx.work.OneTimeWorkRequest
-import androidx.work.Operation
-import androidx.work.WorkManager
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Transformations
+import androidx.work.*
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
 import com.zhuinden.monarchy.Monarchy
+import im.vector.matrix.android.BuildConfig
 import im.vector.matrix.android.api.auth.data.Credentials
 import im.vector.matrix.android.api.session.content.ContentAttachmentData
 import im.vector.matrix.android.api.session.crypto.CryptoService
-import im.vector.matrix.android.api.session.events.model.Event
-import im.vector.matrix.android.api.session.events.model.EventType
-import im.vector.matrix.android.api.session.events.model.isImageMessage
-import im.vector.matrix.android.api.session.events.model.isTextMessage
-import im.vector.matrix.android.api.session.events.model.toModel
+import im.vector.matrix.android.api.session.events.model.*
 import im.vector.matrix.android.api.session.room.model.message.MessageContent
 import im.vector.matrix.android.api.session.room.model.message.MessageType
 import im.vector.matrix.android.api.session.room.send.SendService
 import im.vector.matrix.android.api.session.room.send.SendState
+import im.vector.matrix.android.api.session.room.send.UserDraft
 import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 import im.vector.matrix.android.api.util.Cancelable
 import im.vector.matrix.android.api.util.CancelableBag
+import im.vector.matrix.android.internal.database.RealmLiveData
+import im.vector.matrix.android.internal.database.mapper.DraftMapper
 import im.vector.matrix.android.internal.database.mapper.asDomain
-import im.vector.matrix.android.internal.database.model.EventEntity
-import im.vector.matrix.android.internal.database.model.RoomEntity
-import im.vector.matrix.android.internal.database.model.TimelineEventEntity
+import im.vector.matrix.android.internal.database.model.*
 import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
 import im.vector.matrix.android.internal.database.query.where
 import im.vector.matrix.android.internal.session.content.UploadContentWorker
@@ -75,6 +71,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
     }
 
     private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
+
     override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
         val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
             saveLocalEcho(it)
@@ -165,12 +162,10 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
 
     override fun deleteFailedEcho(localEcho: TimelineEvent) {
         monarchy.writeAsync { realm ->
-            TimelineEventEntity.where(realm, eventId = localEcho.root.eventId
-                                                       ?: "").findFirst()?.let {
+            TimelineEventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let {
                 it.deleteFromRealm()
             }
-            EventEntity.where(realm, eventId = localEcho.root.eventId
-                                               ?: "").findFirst()?.let {
+            EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let {
                 it.deleteFromRealm()
             }
         }
diff --git a/vector/build.gradle b/vector/build.gradle
index 349bf7cf88..697d0f36d0 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -252,8 +252,9 @@ dependencies {
     implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
     implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.0'
     // RXBinding
-    implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0-alpha2'
-    implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0-alpha2'
+    implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0'
+    implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0'
+    implementation 'com.jakewharton.rxbinding3:rxbinding-material:3.0.0'
 
     implementation("com.airbnb.android:epoxy:$epoxy_version")
     kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
index e60bc422a8..dda5f611b6 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt
@@ -18,13 +18,13 @@ package im.vector.riotx.features.home.room.detail
 
 import com.jaiselrahman.filepicker.model.MediaFile
 import im.vector.matrix.android.api.session.events.model.Event
-import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
 import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
 import im.vector.matrix.android.api.session.room.timeline.Timeline
 import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 
 sealed class RoomDetailActions {
 
+    data class SaveDraft(val draft: String) : RoomDetailActions()
     data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
     data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
     data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
@@ -35,13 +35,15 @@ sealed class RoomDetailActions {
     data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
     data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
     data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions()
-    data class HandleTombstoneEvent(val event: Event): RoomDetailActions()
+    data class HandleTombstoneEvent(val event: Event) : RoomDetailActions()
     object AcceptInvite : RoomDetailActions()
     object RejectInvite : RoomDetailActions()
 
     data class EnterEditMode(val eventId: String) : RoomDetailActions()
-    data class EnterQuoteMode(val eventId: String) : RoomDetailActions()
-    data class EnterReplyMode(val eventId: String) : RoomDetailActions()
+    data class EnterQuoteMode(val eventId: String, val draft: String) : RoomDetailActions()
+    data class EnterReplyMode(val eventId: String, val draft: String) : RoomDetailActions()
+    data class ExitSpecialMode(val draft: String) : RoomDetailActions()
+
     data class ResendMessage(val eventId: String) : RoomDetailActions()
     data class RemoveFailedEcho(val eventId: String) : RoomDetailActions()
     object ClearSendQueue : RoomDetailActions()
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
index cd1ccb01d2..b32d08ea7d 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
@@ -53,6 +53,7 @@ import com.google.android.material.snackbar.Snackbar
 import com.jaiselrahman.filepicker.activity.FilePickerActivity
 import com.jaiselrahman.filepicker.config.Configurations
 import com.jaiselrahman.filepicker.model.MediaFile
+import com.jakewharton.rxbinding3.widget.afterTextChangeEvents
 import com.otaliastudios.autocomplete.Autocomplete
 import com.otaliastudios.autocomplete.AutocompleteCallback
 import com.otaliastudios.autocomplete.CharPolicy
@@ -64,7 +65,6 @@ import im.vector.matrix.android.api.session.room.model.message.*
 import im.vector.matrix.android.api.session.room.send.SendState
 import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
-import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
 import im.vector.matrix.android.api.session.user.model.User
 import im.vector.riotx.R
 import im.vector.riotx.core.di.ScreenComponent
@@ -107,6 +107,7 @@ import im.vector.riotx.features.notifications.NotificationDrawerManager
 import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
 import im.vector.riotx.features.settings.VectorPreferences
 import im.vector.riotx.features.themes.ThemeUtils
+import io.reactivex.rxkotlin.subscribeBy
 import kotlinx.android.parcel.Parcelize
 import kotlinx.android.synthetic.main.fragment_room_detail.*
 import kotlinx.android.synthetic.main.merge_composer_layout.view.*
@@ -114,6 +115,7 @@ import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
 import org.commonmark.parser.Parser
 import timber.log.Timber
 import java.io.File
+import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
 
@@ -242,10 +244,10 @@ class RoomDetailFragment :
 
         roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode ->
             when (mode) {
-                SendMode.REGULAR  -> exitSpecialMode()
-                is SendMode.EDIT  -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_edit, true)
-                is SendMode.QUOTE -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_quote, false)
-                is SendMode.REPLY -> enterSpecialMode(mode.timelineEvent, R.drawable.ic_reply, false)
+                is SendMode.REGULAR -> renderRegularMode(mode.text)
+                is SendMode.EDIT    -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, mode.text)
+                is SendMode.QUOTE   -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, mode.text)
+                is SendMode.REPLY   -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, mode.text)
             }
         }
 
@@ -300,14 +302,16 @@ class RoomDetailFragment :
         return super.onOptionsItemSelected(item)
     }
 
-    private fun exitSpecialMode() {
+    private fun renderRegularMode(text: String) {
         commandAutocompletePolicy.enabled = true
         composerLayout.collapse()
+
+        updateComposerText(text)
     }
 
-    private fun enterSpecialMode(event: TimelineEvent,
-                                 @DrawableRes iconRes: Int,
-                                 useText: Boolean) {
+    private fun renderSpecialMode(event: TimelineEvent,
+                                  @DrawableRes iconRes: Int,
+                                  defaultContent: String) {
         commandAutocompletePolicy.enabled = false
         //switch to expanded bar
         composerLayout.composerRelatedMessageTitle.apply {
@@ -321,19 +325,20 @@ class RoomDetailFragment :
         if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
             val parser = Parser.builder().build()
             val document = parser.parse(messageContent.formattedBody
-                                        ?: messageContent.body)
+                    ?: messageContent.body)
             formattedBody = eventHtmlRenderer.render(document)
         }
-        composerLayout.composerRelatedMessageContent.text = formattedBody
-                                                            ?: nonFormattedBody
+        composerLayout.composerRelatedMessageContent.text = formattedBody ?: nonFormattedBody
+
+        updateComposerText(defaultContent)
 
-        composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
         composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
 
-        avatarRenderer.render(event.senderAvatar, event.root.senderId
-                                                  ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
+        avatarRenderer.render(event.senderAvatar,
+                event.root.senderId ?: "",
+                event.senderName,
+                composerLayout.composerRelatedMessageAvatar)
 
-        composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
         composerLayout.expand {
             //need to do it here also when not using quick reply
             focusComposerAndShowKeyboard()
@@ -341,6 +346,16 @@ class RoomDetailFragment :
         focusComposerAndShowKeyboard()
     }
 
+    private fun updateComposerText(text: String) {
+        // Do not update if this is the same text to avoid the cursor to move
+        if (text != composerLayout.composerEditText.text.toString()) {
+            // Ignore update to avoid saving a draft
+            filterComposerTextChange = true
+            composerLayout.composerEditText.setText(text)
+            composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
+        }
+    }
+
     override fun onResume() {
         super.onResume()
 
@@ -360,9 +375,9 @@ class RoomDetailFragment :
                 REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
                 REACTION_SELECT_REQUEST_CODE                        -> {
                     val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
-                                  ?: return
+                            ?: return
                     val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
-                                   ?: return
+                            ?: return
                     //TODO check if already reacted with that?
                     roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
                 }
@@ -397,32 +412,46 @@ class RoomDetailFragment :
 
         if (vectorPreferences.swipeToReplyIsEnabled()) {
             val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
-                                                               R.drawable.ic_reply,
-                                                               object : RoomMessageTouchHelperCallback.QuickReplayHandler {
-                                                                   override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
-                                                                       (model as? AbsMessageItem)?.informationData?.let {
-                                                                           val eventId = it.eventId
-                                                                           roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
-                                                                       }
-                                                                   }
+                    R.drawable.ic_reply,
+                    object : RoomMessageTouchHelperCallback.QuickReplayHandler {
+                        override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
+                            (model as? AbsMessageItem)?.informationData?.let {
+                                val eventId = it.eventId
+                                roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId, composerLayout.composerEditText.text.toString()))
+                            }
+                        }
 
-                                                                   override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
-                                                                       return when (model) {
-                                                                           is MessageFileItem,
-                                                                           is MessageImageVideoItem,
-                                                                           is MessageTextItem -> {
-                                                                               return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
-                                                                           }
-                                                                           else               -> false
-                                                                       }
-                                                                   }
-                                                               })
+                        override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
+                            return when (model) {
+                                is MessageFileItem,
+                                is MessageImageVideoItem,
+                                is MessageTextItem -> {
+                                    return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
+                                }
+                                else               -> false
+                            }
+                        }
+                    })
             val touchHelper = ItemTouchHelper(swipeCallback)
             touchHelper.attachToRecyclerView(recyclerView)
         }
     }
 
+    private var filterComposerTextChange = true
+
     private fun setupComposer() {
+        composerLayout.composerEditText.afterTextChangeEvents()
+                .debounce(100, TimeUnit.MILLISECONDS)
+                .subscribeBy {
+                    if (filterComposerTextChange) {
+                        Timber.d("Draft: ignore text update")
+                        filterComposerTextChange = false
+                        return@subscribeBy
+                    }
+                    roomDetailViewModel.process(RoomDetailActions.SaveDraft(it.editable.toString()))
+                }
+                .disposeOnDestroy()
+
         val elevation = 6f
         val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background))
         Autocomplete.on<Command>(composerLayout.composerEditText)
@@ -492,8 +521,7 @@ class RoomDetailFragment :
             }
         }
         composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
-            composerLayout.composerEditText.setText("")
-            roomDetailViewModel.resetSendMode()
+            roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString()))
         }
     }
 
@@ -645,13 +673,11 @@ class RoomDetailFragment :
     private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
         when (sendMessageResult) {
             is SendMessageResult.MessageSent                -> {
-                // Clear composer
-                composerLayout.composerEditText.text = null
+                // Nothing to do, the composer will be cleared with the draft update
             }
             is SendMessageResult.SlashCommandHandled        -> {
                 sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
-                // Clear composer
-                composerLayout.composerEditText.text = null
+                // The composer will be cleared with the draft update
             }
             is SendMessageResult.SlashCommandError          -> {
                 displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
@@ -916,10 +942,10 @@ class RoomDetailFragment :
                 roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId))
             }
             is SimpleAction.Quote               -> {
-                roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId))
+                roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString()))
             }
             is SimpleAction.Reply               -> {
-                roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId))
+                roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString()))
             }
             is SimpleAction.CopyPermalink       -> {
                 val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
index 8ec133c642..593a3dfd04 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
@@ -42,8 +42,9 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent
 import im.vector.matrix.android.api.session.room.model.message.MessageType
 import im.vector.matrix.android.api.session.room.model.message.getFileUrl
 import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
-import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.matrix.android.api.session.room.send.UserDraft
 import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
+import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
 import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
 import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
 import im.vector.matrix.rx.rx
@@ -84,6 +85,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 
     private var timeline = room.createTimeline(eventId, timelineSettings)
 
+    // Filter to avoid infinite loop when user enter text in the composer and call SaveDraft
+    private var filterDraftUpdate = false
+
     // Slot to keep a pending action during permission request
     var pendingAction: RoomDetailActions? = null
 
@@ -109,6 +113,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         observeRoomSummary()
         observeEventDisplayedActions()
         observeSummaryState()
+        observeDrafts()
         room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
         timeline.start()
         setState { copy(timeline = this@RoomDetailViewModel.timeline) }
@@ -116,6 +121,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 
     fun process(action: RoomDetailActions) {
         when (action) {
+            is RoomDetailActions.SaveDraft              -> handleSaveDraft(action)
             is RoomDetailActions.SendMessage            -> handleSendMessage(action)
             is RoomDetailActions.SendMedia              -> handleSendMedia(action)
             is RoomDetailActions.EventDisplayed         -> handleEventDisplayed(action)
@@ -129,6 +135,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
             is RoomDetailActions.EnterEditMode          -> handleEditAction(action)
             is RoomDetailActions.EnterQuoteMode         -> handleQuoteAction(action)
             is RoomDetailActions.EnterReplyMode         -> handleReplyAction(action)
+            is RoomDetailActions.ExitSpecialMode        -> handleExitSpecialMode(action)
             is RoomDetailActions.DownloadFile           -> handleDownloadFile(action)
             is RoomDetailActions.NavigateToEvent        -> handleNavigateToEvent(action)
             is RoomDetailActions.HandleTombstoneEvent   -> handleTombstoneEvent(action)
@@ -140,9 +147,64 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     }
 
+    /**
+     * Convert a send mode to a draft and save the draft
+     */
+    private fun handleSaveDraft(action: RoomDetailActions.SaveDraft) {
+        // The text is changed, ignore the next update from DB
+        filterDraftUpdate = true
+
+        withState {
+            when (it.sendMode) {
+                is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft))
+                is SendMode.REPLY   -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft))
+                is SendMode.QUOTE   -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft))
+                is SendMode.EDIT    -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft))
+            }
+        }
+    }
+
+    private fun observeDrafts() {
+        room.rx().liveDrafts()
+                .subscribe {
+                    Timber.d("Draft update!")
+                    if (filterDraftUpdate) {
+                        Timber.d(" --> Ignore")
+                        return@subscribe
+                    }
+
+                    Timber.d(" --> SetState")
+
+                    setState {
+                        val draft = it.lastOrNull() ?: UserDraft.REGULAR("")
+                        copy(
+                                // Create a sendMode from a draft and retrieve the TimelineEvent
+                                sendMode = when (draft) {
+                                    is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
+                                    is UserDraft.QUOTE   -> {
+                                        room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
+                                            SendMode.QUOTE(timelineEvent, draft.text)
+                                        }
+                                    }
+                                    is UserDraft.REPLY   -> {
+                                        room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
+                                            SendMode.REPLY(timelineEvent, draft.text)
+                                        }
+                                    }
+                                    is UserDraft.EDIT    -> {
+                                        room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
+                                            SendMode.EDIT(timelineEvent, draft.text)
+                                        }
+                                    }
+                                } ?: SendMode.REGULAR("")
+                        )
+                    }
+                }
+                .disposeOnClear()
+    }
+
     private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) {
-        val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
-                               ?: return
+        val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>() ?: return
 
         val roomId = tombstoneContent.replacementRoom ?: ""
         val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
@@ -166,22 +228,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 
     }
 
-    private fun enterEditMode(event: TimelineEvent) {
-        setState {
-            copy(
-                    sendMode = SendMode.EDIT(event)
-            )
-        }
-    }
-
-    fun resetSendMode() {
-        setState {
-            copy(
-                    sendMode = SendMode.REGULAR
-            )
-        }
-    }
-
     private val _nonBlockingPopAlert = MutableLiveData<LiveEvent<Pair<Int, List<Any>>>>()
     val nonBlockingPopAlert: LiveData<LiveEvent<Pair<Int, List<Any>>>>
         get() = _nonBlockingPopAlert
@@ -218,7 +264,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
         withState { state ->
             when (state.sendMode) {
-                SendMode.REGULAR  -> {
+                is SendMode.REGULAR -> {
                     val slashCommandResult = CommandParser.parseSplashCommand(action.text)
 
                     when (slashCommandResult) {
@@ -226,6 +272,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                             // Send the text message to the room
                             room.sendTextMessage(action.text, autoMarkdown = action.autoMarkdown)
                             _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
+                            popDraft()
                         }
                         is ParsedCommand.ErrorSyntax              -> {
                             _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))
@@ -238,6 +285,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                         }
                         is ParsedCommand.Invite                   -> {
                             handleInviteSlashCommand(slashCommandResult)
+                            popDraft()
                         }
                         is ParsedCommand.SetUserPowerLevel        -> {
                             // TODO
@@ -251,6 +299,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                             vectorPreferences.setMarkdownEnabled(slashCommandResult.enable)
                             _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled(
                                     if (slashCommandResult.enable) R.string.markdown_has_been_enabled else R.string.markdown_has_been_disabled))
+                            popDraft()
                         }
                         is ParsedCommand.UnbanUser                -> {
                             // TODO
@@ -275,9 +324,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                         is ParsedCommand.SendEmote                -> {
                             room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE)
                             _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
+                            popDraft()
                         }
                         is ParsedCommand.ChangeTopic              -> {
                             handleChangeTopicSlashCommand(slashCommandResult)
+                            popDraft()
                         }
                         is ParsedCommand.ChangeDisplayName        -> {
                             // TODO
@@ -285,11 +336,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                         }
                     }
                 }
-                is SendMode.EDIT  -> {
+                is SendMode.EDIT    -> {
 
                     //is original event a reply?
                     val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
-                                    ?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
+                            ?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
                     if (inReplyTo != null) {
                         //TODO check if same content?
                         room.getTimeLineEvent(inReplyTo)?.let {
@@ -298,27 +349,24 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                     } else {
                         val messageContent: MessageContent? =
                                 state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
-                                ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
+                                        ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
                         val existingBody = messageContent?.body ?: ""
                         if (existingBody != action.text) {
-                            room.editTextMessage(state.sendMode.timelineEvent.root.eventId
-                                                 ?: "", messageContent?.type
-                                                        ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
+                            room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
+                                    messageContent?.type ?: MessageType.MSGTYPE_TEXT,
+                                    action.text,
+                                    action.autoMarkdown)
                         } else {
                             Timber.w("Same message content, do not send edition")
                         }
                     }
-                    setState {
-                        copy(
-                                sendMode = SendMode.REGULAR
-                        )
-                    }
                     _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
+                    popDraft()
                 }
-                is SendMode.QUOTE -> {
+                is SendMode.QUOTE   -> {
                     val messageContent: MessageContent? =
                             state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
-                            ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
+                                    ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
                     val textMsg = messageContent?.body
 
                     val finalText = legacyRiotQuoteText(textMsg, action.text)
@@ -333,29 +381,25 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                     } else {
                         room.sendFormattedTextMessage(finalText, htmlText)
                     }
-                    setState {
-                        copy(
-                                sendMode = SendMode.REGULAR
-                        )
-                    }
                     _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
+                    popDraft()
                 }
-                is SendMode.REPLY -> {
+                is SendMode.REPLY   -> {
                     state.sendMode.timelineEvent.let {
                         room.replyToMessage(it, action.text, action.autoMarkdown)
-                        setState {
-                            copy(
-                                    sendMode = SendMode.REGULAR
-                            )
-                        }
                         _sendMessageResultLiveData.postLiveEvent(SendMessageResult.MessageSent)
+                        popDraft()
                     }
-
                 }
             }
         }
     }
 
+    private fun popDraft() {
+        filterDraftUpdate = false
+        room.deleteDraft()
+    }
+
     private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
         val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
         var quotedTextMsg = StringBuilder()
@@ -469,27 +513,56 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     }
 
     private fun handleEditAction(action: RoomDetailActions.EnterEditMode) {
-        room.getTimeLineEvent(action.eventId)?.let {
-            enterEditMode(it)
+        room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
+            timelineEvent.root.eventId?.let {
+                filterDraftUpdate = false
+                room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: ""))
+            }
         }
     }
 
     private fun handleQuoteAction(action: RoomDetailActions.EnterQuoteMode) {
-        room.getTimeLineEvent(action.eventId)?.let {
-            setState {
-                copy(
-                        sendMode = SendMode.QUOTE(it)
-                )
+        room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
+            withState { state ->
+                // Save a new draft and keep the previously entered text, if it was not an edit
+                timelineEvent.root.eventId?.let {
+                    filterDraftUpdate = false
+                    if (state.sendMode is SendMode.EDIT) {
+                        room.saveDraft(UserDraft.QUOTE(it, ""))
+                    } else {
+                        room.saveDraft(UserDraft.QUOTE(it, action.draft))
+                    }
+                }
             }
         }
     }
 
     private fun handleReplyAction(action: RoomDetailActions.EnterReplyMode) {
-        room.getTimeLineEvent(action.eventId)?.let {
-            setState {
-                copy(
-                        sendMode = SendMode.REPLY(it)
-                )
+        room.getTimeLineEvent(action.eventId)?.let { timelineEvent ->
+            withState { state ->
+                // Save a new draft and keep the previously entered text, if it was not an edit
+                timelineEvent.root.eventId?.let {
+                    filterDraftUpdate = false
+                    if (state.sendMode is SendMode.EDIT) {
+                        room.saveDraft(UserDraft.REPLY(it, ""))
+                    } else {
+                        room.saveDraft(UserDraft.REPLY(it, action.draft))
+                    }
+                }
+            }
+        }
+    }
+
+    private fun handleExitSpecialMode(action: RoomDetailActions.ExitSpecialMode) {
+        withState { state ->
+            // For edit, just delete the current draft
+            filterDraftUpdate = false
+
+            if (state.sendMode is SendMode.EDIT) {
+                room.deleteDraft()
+            } else {
+                // Save a new draft and keep the previously entered text
+                room.saveDraft(UserDraft.REGULAR(action.draft))
             }
         }
     }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
index d8358efe16..a47ee56500 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
@@ -34,11 +34,11 @@ import im.vector.matrix.android.api.session.user.model.User
  *
  * Depending on the state the bottom toolbar will change (icons/preview/actions...)
  */
-sealed class SendMode {
-    object REGULAR : SendMode()
-    data class QUOTE(val timelineEvent: TimelineEvent) : SendMode()
-    data class EDIT(val timelineEvent: TimelineEvent) : SendMode()
-    data class REPLY(val timelineEvent: TimelineEvent) : SendMode()
+sealed class SendMode(open val text: String) {
+    data class REGULAR(override val text: String) : SendMode(text)
+    data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
+    data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
+    data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
 }
 
 data class RoomDetailViewState(
@@ -47,7 +47,7 @@ data class RoomDetailViewState(
         val timeline: Timeline? = null,
         val asyncInviter: Async<User> = Uninitialized,
         val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
-        val sendMode: SendMode = SendMode.REGULAR,
+        val sendMode: SendMode = SendMode.REGULAR(""),
         val isEncrypted: Boolean = false,
         val tombstoneEvent: Event? = null,
         val tombstoneEventHandling: Async<String> = Uninitialized,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt
index 2ee1f30645..f5b62e4512 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt
@@ -40,6 +40,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
     @EpoxyAttribute var avatarUrl: String? = null
     @EpoxyAttribute var unreadNotificationCount: Int = 0
     @EpoxyAttribute var hasUnreadMessage: Boolean = false
+    @EpoxyAttribute var hasDraft: Boolean = false
     @EpoxyAttribute var showHighlighted: Boolean = false
     @EpoxyAttribute var listener: (() -> Unit)? = null
 
@@ -52,6 +53,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
         holder.lastEventView.text = lastFormattedEvent
         holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
         holder.unreadIndentIndicator.isVisible = hasUnreadMessage
+        holder.draftView.isVisible = hasDraft
         avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
     }
 
@@ -60,6 +62,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
         val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
         val unreadIndentIndicator by bind<View>(R.id.roomUnreadIndicator)
         val lastEventView by bind<TextView>(R.id.roomLastEventView)
+        val draftView by bind<ImageView>(R.id.roomDraftBadge)
         val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
         val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
         val rootView by bind<ViewGroup>(R.id.itemRoomLayout)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
index 015e54b368..942796961b 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt
@@ -133,6 +133,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
                 .showHighlighted(showHighlighted)
                 .unreadNotificationCount(unreadCount)
                 .hasUnreadMessage(roomSummary.hasUnreadMessages)
+                .hasDraft(roomSummary.userDrafts.isNotEmpty())
                 .listener { listener?.onRoomSelected(roomSummary) }
     }
 
diff --git a/vector/src/main/res/layout/item_room.xml b/vector/src/main/res/layout/item_room.xml
index 7eb2083ecb..741bd47069 100644
--- a/vector/src/main/res/layout/item_room.xml
+++ b/vector/src/main/res/layout/item_room.xml
@@ -56,13 +56,27 @@
         android:textSize="15sp"
         android:textStyle="bold"
         app:layout_constrainedWidth="true"
-        app:layout_constraintEnd_toStartOf="@+id/roomUnreadCounterBadgeView"
+        app:layout_constraintEnd_toStartOf="@+id/roomDraftBadge"
         app:layout_constraintHorizontal_bias="0.0"
         app:layout_constraintHorizontal_chainStyle="packed"
         app:layout_constraintStart_toEndOf="@id/roomAvatarImageView"
         app:layout_constraintTop_toTopOf="parent"
         tools:text="@sample/matrix.json/data/displayName" />
 
+    <ImageView
+        android:id="@+id/roomDraftBadge"
+        android:layout_width="16dp"
+        android:layout_height="16dp"
+        android:layout_marginLeft="4dp"
+        android:layout_marginRight="4dp"
+        android:src="@drawable/ic_edit"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="@+id/roomNameView"
+        app:layout_constraintEnd_toStartOf="@+id/roomUnreadCounterBadgeView"
+        app:layout_constraintStart_toEndOf="@+id/roomNameView"
+        app:layout_constraintTop_toTopOf="@+id/roomNameView"
+        tools:visibility="visible" />
+
     <im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
         android:id="@+id/roomUnreadCounterBadgeView"
         android:layout_width="wrap_content"
@@ -76,12 +90,14 @@
         android:paddingRight="4dp"
         android:textColor="@android:color/white"
         android:textSize="10sp"
+        android:visibility="gone"
         app:layout_constraintBottom_toBottomOf="@+id/roomNameView"
         app:layout_constraintEnd_toStartOf="@+id/roomLastEventTimeView"
-        app:layout_constraintStart_toEndOf="@+id/roomNameView"
+        app:layout_constraintStart_toEndOf="@+id/roomDraftBadge"
         app:layout_constraintTop_toTopOf="@+id/roomNameView"
         tools:background="@drawable/bg_unread_highlight"
-        tools:text="4" />
+        tools:text="4"
+        tools:visibility="visible" />
 
     <TextView
         android:id="@+id/roomLastEventTimeView"