From 000db4b19231566e1989894cfacaeeea528fd251 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Fri, 26 Jul 2019 16:02:20 +0200
Subject: [PATCH] Basic Message Failure support + Resend (text only)

+ clean worker inputs when starting new independent task in unique queue
---
 CHANGES.md                                    |   1 +
 .../android/api/session/events/model/Event.kt |  38 ++++
 .../api/session/room/send/SendService.kt      |  28 +++
 .../api/session/room/send/SendState.kt        |   4 +
 .../api/session/room/timeline/Timeline.kt     |   3 +
 .../session/room/timeline/TimelineEvent.kt    |   1 -
 .../internal/database/mapper/EventMapper.kt   |   1 +
 .../database/mapper/TimelineEventMapper.kt    |   3 +-
 .../session/content/UploadContentWorker.kt    |  10 +-
 .../session/room/prune/PruneEventTask.kt      |   3 +-
 .../session/room/send/DefaultSendService.kt   | 185 ++++++++++++++++--
 .../session/room/send/EncryptEventWorker.kt   |  10 +-
 .../session/room/send/FakeSendWorker.kt       |  28 +++
 .../session/room/send/LocalEchoUpdater.kt     |   8 +-
 .../internal/session/room/send/NoMerger.kt    |  25 +++
 .../session/room/send/SendEventWorker.kt      |   5 +-
 .../session/room/timeline/DefaultTimeline.kt  |  17 ++
 .../timeline/TimelineSendEventWorkCommon.kt   |   8 +-
 .../src/main/res/values/strings_RiotX.xml     |   3 +-
 .../riotx/core/extensions/TimelineEvent.kt    |   2 +-
 .../home/room/detail/RoomDetailActions.kt     |   4 +
 .../home/room/detail/RoomDetailActivity.kt    |   5 +
 .../home/room/detail/RoomDetailFragment.kt    |  36 +++-
 .../home/room/detail/RoomDetailViewModel.kt   |  64 +++++-
 .../detail/RoomMessageTouchHelperCallback.kt  |   3 +-
 .../action/MessageActionsBottomSheet.kt       |  13 ++
 .../timeline/action/MessageMenuViewModel.kt   |  65 +++---
 .../timeline/factory/EncryptionItemFactory.kt |   2 +-
 .../timeline/factory/MessageItemFactory.kt    |   4 +-
 .../timeline/factory/NoticeItemFactory.kt     |   2 +-
 .../timeline/factory/TimelineItemFactory.kt   |   2 +-
 .../detail/timeline/item/AbsMessageItem.kt    |   7 +-
 .../timeline/item/MessageImageVideoItem.kt    |  11 +-
 .../util/MessageInformationDataFactory.kt     |   2 +-
 .../src/main/res/drawable/ic_refresh_cw.xml   |  22 +++
 vector/src/main/res/drawable/ic_trash.xml     |  14 ++
 .../main/res/drawable/ic_warning_small.xml    |  14 ++
 .../layout/bottom_sheet_message_actions.xml   |  32 +++
 ...item_timeline_event_media_message_stub.xml |  14 +-
 vector/src/main/res/menu/menu_timeline.xml    |  29 +++
 .../res/menu/vector_room_message_settings.xml |  89 ---------
 vector/src/main/res/values/strings.xml        |   2 +-
 42 files changed, 661 insertions(+), 158 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/FakeSendWorker.kt
 create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/NoMerger.kt
 create mode 100644 vector/src/main/res/drawable/ic_refresh_cw.xml
 create mode 100644 vector/src/main/res/drawable/ic_trash.xml
 create mode 100644 vector/src/main/res/drawable/ic_warning_small.xml
 create mode 100644 vector/src/main/res/menu/menu_timeline.xml
 delete mode 100755 vector/src/main/res/menu/vector_room_message_settings.xml

diff --git a/CHANGES.md b/CHANGES.md
index 5de98507ca..a85c2ff60f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,6 +7,7 @@ Features:
 Improvements:
  - UI for pending edits (#193)
  - UX image preview screen transition (#393)
+ - Basic support for resending failed messages (retry/remove)
 
 Other changes:
  -
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt
index 547e627fff..7dd0c6e2ef 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt
@@ -20,6 +20,9 @@ import android.text.TextUtils
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 import im.vector.matrix.android.api.session.crypto.MXCryptoError
+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.SendState
 import im.vector.matrix.android.api.util.JsonDict
 import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
 import im.vector.matrix.android.internal.di.MoshiProvider
@@ -81,6 +84,7 @@ data class Event(
 
     var mxDecryptionResult: OlmDecryptionResult? = null
     var mCryptoError: MXCryptoError.ErrorType? = null
+    var sendState: SendState = SendState.UNKNOWN
 
 
     /**
@@ -272,6 +276,7 @@ data class Event(
         if (redacts != other.redacts) return false
         if (mxDecryptionResult != other.mxDecryptionResult) return false
         if (mCryptoError != other.mCryptoError) return false
+        if (sendState != other.sendState) return false
 
         return true
     }
@@ -289,6 +294,39 @@ data class Event(
         result = 31 * result + (redacts?.hashCode() ?: 0)
         result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
         result = 31 * result + (mCryptoError?.hashCode() ?: 0)
+        result = 31 * result + sendState.hashCode()
         return result
     }
+
+}
+
+
+fun Event.isTextMessage(): Boolean {
+    if (this.getClearType() == EventType.MESSAGE) {
+        return getClearContent()?.toModel<MessageContent>()?.let {
+            when (it.type) {
+                MessageType.MSGTYPE_TEXT,
+                MessageType.MSGTYPE_EMOTE,
+                MessageType.MSGTYPE_NOTICE -> {
+                    true
+                }
+                else                       -> false
+            }
+        } ?: false
+    }
+    return false
+}
+
+fun Event.isImageMessage(): Boolean {
+    if (this.getClearType() == EventType.MESSAGE) {
+        return getClearContent()?.toModel<MessageContent>()?.let {
+            when (it.type) {
+                MessageType.MSGTYPE_IMAGE -> {
+                    true
+                }
+                else                      -> false
+            }
+        } ?: false
+    }
+    return false
 }
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt
index 94abd5d31d..ae276adb73 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt
@@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.send
 import im.vector.matrix.android.api.session.content.ContentAttachmentData
 import im.vector.matrix.android.api.session.events.model.Event
 import im.vector.matrix.android.api.session.room.model.message.MessageType
+import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 import im.vector.matrix.android.api.util.Cancelable
 
 
@@ -65,4 +66,31 @@ interface SendService {
      */
     fun redactEvent(event: Event, reason: String?): Cancelable
 
+
+    /**
+     * Schedule this message to be resent
+     * @param localEcho the unsent local echo
+     */
+    fun resendTextMessage(localEcho: TimelineEvent): Cancelable?
+
+    /**
+     * Schedule this message to be resent
+     * @param localEcho the unsent local echo
+     */
+    fun resendMediaMessage(localEcho: TimelineEvent): Cancelable?
+
+
+    /**
+     * Remove this failed message from the timeline
+     * @param localEcho the unsent local echo
+     */
+    fun deleteFailedEcho(localEcho: TimelineEvent)
+
+    fun clearSendingQueue()
+
+    /**
+     * Resend all failed messages one by one (and keep order)
+     */
+    fun resendAllFailedMessages()
+
 }
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt
index 75e3c0f665..e9f22da472 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendState.kt
@@ -41,4 +41,8 @@ enum class SendState {
         return this == UNDELIVERED || this == FAILED_UNKNOWN_DEVICES
     }
 
+    fun isSending(): Boolean {
+        return this == UNSENT || this == ENCRYPTING || this == SENDING
+    }
+
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
index e52ac3b48d..314c9f61b8 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
@@ -56,6 +56,9 @@ interface Timeline {
      */
     fun paginate(direction: Direction, count: Int)
 
+    fun pendingEventCount() : Int
+
+    fun failedToDeliverEventCount() : Int
 
     interface Listener {
         /**
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt
index 044aa957f3..ef2769d9bf 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt
@@ -38,7 +38,6 @@ data class TimelineEvent(
         val senderName: String?,
         val isUniqueDisplayName: Boolean,
         val senderAvatar: String?,
-        val sendState: SendState,
         val annotations: EventAnnotationsSummary? = null
 ) {
 
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt
index 30346f789e..0d76b548fe 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt
@@ -73,6 +73,7 @@ internal object EventMapper {
                 unsignedData = ud,
                 redacts = eventEntity.redacts
         ).also {
+            it.sendState = eventEntity.sendState
             eventEntity.decryptionResultJson?.let { json ->
                 try {
                     it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt
index 92cbd4be82..fa06799916 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt
@@ -33,8 +33,7 @@ internal object TimelineEventMapper {
                 displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
                 senderName = timelineEventEntity.senderName,
                 isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
-                senderAvatar = timelineEventEntity.senderAvatar,
-                sendState = timelineEventEntity.root?.sendState ?: SendState.UNKNOWN
+                senderAvatar = timelineEventEntity.senderAvatar
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt
index 8e1a028186..b015670daa 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt
@@ -57,9 +57,11 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
     override suspend fun doWork(): Result {
         val params = WorkerParamsFactory.fromData<Params>(inputData)
                 ?: return Result.success()
+        Timber.v("Starting upload media work with params $params")
 
         if (params.lastFailureMessage != null) {
             // Transmit the error
+            Timber.v("Stop upload media work due to input failure")
             return Result.success(inputData)
         }
 
@@ -121,7 +123,11 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
 
         val progressListener = object : ProgressRequestBody.Listener {
             override fun onProgress(current: Long, total: Long) {
-                contentUploadStateTracker.setProgress(eventId, current, total)
+                if (isStopped) {
+                    contentUploadStateTracker.setFailure(eventId, Throwable("Cancelled"))
+                } else {
+                    contentUploadStateTracker.setProgress(eventId, current, total)
+                }
             }
         }
 
@@ -166,6 +172,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
                               encryptedFileInfo: EncryptedFileInfo?,
                               thumbnailUrl: String?,
                               thumbnailEncryptedFileInfo: EncryptedFileInfo?): Result {
+        Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped")
         contentUploadStateTracker.setSuccess(params.event.eventId!!)
         val event = updateEvent(params.event, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
         val sendParams = SendEventWorker.Params(params.userId, params.roomId, event)
@@ -210,6 +217,7 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
         )
     }
 
+
     private fun MessageFileContent.update(url: String,
                                           encryptedFileInfo: EncryptedFileInfo?): MessageFileContent {
         return copy(
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt
index 24dc14a72f..ab373a6ef4 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt
@@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity
 import im.vector.matrix.android.internal.database.query.findWithSenderMembershipEvent
 import im.vector.matrix.android.internal.database.query.where
 import im.vector.matrix.android.internal.di.MoshiProvider
+import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
 import im.vector.matrix.android.internal.task.Task
 import im.vector.matrix.android.internal.util.tryTransactionSync
 import io.realm.Realm
@@ -63,7 +64,7 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
         val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId
                 ?: "").findFirst()
                 ?: return
-        val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT
+        val isLocalEcho = LocalEchoEventFactory.isLocalEchoId(redactionEvent.eventId ?: "")
         Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")
 
         val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
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 9a94b05b1f..8adb45ddc0 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,25 +17,36 @@
 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.WorkManager
+import androidx.work.*
 import com.zhuinden.monarchy.Monarchy
 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.isTextMessage
+import im.vector.matrix.android.api.session.events.model.toModel
+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.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.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.query.where
 import im.vector.matrix.android.internal.session.content.UploadContentWorker
 import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
 import im.vector.matrix.android.internal.util.CancelableWork
+import im.vector.matrix.android.internal.util.tryTransactionAsync
 import im.vector.matrix.android.internal.worker.WorkManagerUtil
 import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
 import im.vector.matrix.android.internal.worker.WorkerParamsFactory
 import timber.log.Timber
+import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
@@ -50,6 +61,7 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
                                                       private val monarchy: Monarchy)
     : SendService {
 
+    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)
@@ -70,7 +82,7 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
         // Encrypted room handling
         return if (cryptoService.isRoomEncrypted(roomId)) {
             Timber.v("Send event in encrypted room")
-            val encryptWork = createEncryptEventWork(event)
+            val encryptWork = createEncryptEventWork(event, true)
             val sendWork = createSendEventWork(event)
             TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, sendWork)
             CancelableWork(context, encryptWork.id)
@@ -94,25 +106,162 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
         return CancelableWork(context, redactWork.id)
     }
 
+    override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
+        if (localEcho.root.isTextMessage()) {
+            return sendEvent(localEcho.root)
+        }
+        return null
+
+    }
+
+    override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? {
+        //TODO this need a refactoring of attachement sending
+//        val clearContent = localEcho.root.getClearContent()
+//        val messageContent = clearContent?.toModel<MessageContent>() ?: return null
+//        when (messageContent.type) {
+//            MessageType.MSGTYPE_IMAGE -> {
+//                val imageContent = clearContent.toModel<MessageImageContent>() ?: return null
+//                val url = imageContent.url ?: return null
+//                if (url.startsWith("mxc://")) {
+//                    //TODO
+//                } else {
+//                    //The image has not yet been sent
+//                    val attachmentData = ContentAttachmentData(
+//                            size = imageContent.info!!.size.toLong(),
+//                            mimeType = imageContent.info.mimeType!!,
+//                            width = imageContent.info.width.toLong(),
+//                            height = imageContent.info.height.toLong(),
+//                            name = imageContent.body,
+//                            path = imageContent.url,
+//                            type = ContentAttachmentData.Type.IMAGE
+//                    )
+//                    monarchy.runTransactionSync {
+//                        EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let {
+//                            it.sendState = SendState.UNSENT
+//                        }
+//                    }
+//                    return internalSendMedia(localEcho.root,attachmentData)
+//                }
+//            }
+//        }
+        return null
+
+    }
+
+    override fun deleteFailedEcho(localEcho: TimelineEvent) {
+        monarchy.tryTransactionAsync { realm ->
+            TimelineEventEntity.where(realm, eventId = localEcho.root.eventId
+                    ?: "").findFirst()?.let {
+                it.deleteFromRealm()
+            }
+            EventEntity.where(realm, eventId = localEcho.root.eventId
+                    ?: "").findFirst()?.let {
+                it.deleteFromRealm()
+            }
+        }
+    }
+
+    override fun clearSendingQueue() {
+        TimelineSendEventWorkCommon.cancelAllWorks(context, roomId)
+        WorkManager.getInstance(context).cancelUniqueWork(buildWorkIdentifier(UPLOAD_WORK))
+
+        matrixOneTimeWorkRequestBuilder<FakeSendWorker>()
+                .build().let {
+                    TimelineSendEventWorkCommon.postWork(context, roomId, it, ExistingWorkPolicy.REPLACE)
+
+                    //need to clear also image sending queue
+                    WorkManager.getInstance(context)
+                            .beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
+                            .enqueue()
+                }
+
+        monarchy.tryTransactionAsync { realm ->
+            RoomEntity.where(realm, roomId).findFirst()?.let { room ->
+                room.sendingTimelineEvents.forEach {
+                    it.root?.sendState = SendState.UNDELIVERED
+                }
+            }
+        }
+
+    }
+
+    override fun resendAllFailedMessages() {
+        monarchy.tryTransactionAsync { realm ->
+            RoomEntity.where(realm, roomId).findFirst()?.let { room ->
+                room.sendingTimelineEvents.filter {
+                    it.root?.sendState?.hasFailed() ?: false
+                }.sortedBy { it.root?.originServerTs ?: 0 }.forEach { timelineEventEntity ->
+                    timelineEventEntity.root?.let {
+                        val event = it.asDomain()
+                        when (event.getClearType()) {
+                            EventType.MESSAGE,
+                            EventType.REDACTION,
+                            EventType.REACTION -> {
+                                val content = event.getClearContent().toModel<MessageContent>()
+                                if (content != null) {
+                                    when (content.type) {
+                                        MessageType.MSGTYPE_EMOTE,
+                                        MessageType.MSGTYPE_NOTICE,
+                                        MessageType.MSGTYPE_LOCATION,
+                                        MessageType.MSGTYPE_TEXT  -> {
+                                            it.sendState = SendState.UNSENT
+                                            sendEvent(event)
+                                        }
+                                        MessageType.MSGTYPE_FILE,
+                                        MessageType.MSGTYPE_VIDEO,
+                                        MessageType.MSGTYPE_IMAGE,
+                                        MessageType.MSGTYPE_AUDIO -> {
+                                            //need to resend the attachement
+                                        }
+                                        else                      -> {
+                                            Timber.e("Cannot resend message ${event.type} / ${content.type}")
+                                        }
+
+                                    }
+                                } else {
+                                    Timber.e("Unsupported message to resend ${event.type}")
+                                }
+                            }
+                            else               -> {
+                                Timber.e("Unsupported message to resend ${event.type}")
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
         // Create an event with the media file path
         val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
             saveLocalEcho(it)
         }
 
+        return internalSendMedia(event, attachment)
+    }
+
+    private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): CancelableWork {
         val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId)
 
-        val uploadWork = createUploadMediaWork(event, attachment, isRoomEncrypted)
-        val sendWork = createSendEventWork(event)
+        val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true)
+        val sendWork = createSendEventWork(localEcho)
 
         if (isRoomEncrypted) {
-            val encryptWork = createEncryptEventWork(event)
+            val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/)
 
-            WorkManager.getInstance(context)
+            val op: Operation = WorkManager.getInstance(context)
                     .beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
                     .then(encryptWork)
                     .then(sendWork)
                     .enqueue()
+            op.result.addListener(Runnable {
+                if (op.result.isCancelled) {
+                    Timber.e("CHAINE WAS CANCELLED")
+                } else if (op.state.value is Operation.State.FAILURE) {
+                    Timber.e("CHAINE DID FAIL")
+                }
+            }, workerFutureListenerExecutor)
         } else {
             WorkManager.getInstance(context)
                     .beginUniqueWork(buildWorkIdentifier(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork)
@@ -131,7 +280,7 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
         return "${roomId}_$identifier"
     }
 
-    private fun createEncryptEventWork(event: Event): OneTimeWorkRequest {
+    private fun createEncryptEventWork(event: Event, startChain: Boolean = false): OneTimeWorkRequest {
         // Same parameter
         val params = EncryptEventWorker.Params(credentials.userId, roomId, event)
         val sendWorkData = WorkerParamsFactory.toData(params)
@@ -139,6 +288,11 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
         return matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
                 .setConstraints(WorkManagerUtil.workConstraints)
                 .setInputData(sendWorkData)
+                .apply {
+                    if (startChain) {
+                        setInputMerger(NoMerger::class.java)
+                    }
+                }
                 .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
                 .build()
     }
@@ -159,15 +313,24 @@ internal class DefaultSendService @Inject constructor(private val context: Conte
         return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
     }
 
-    private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData, isRoomEncrypted: Boolean): OneTimeWorkRequest {
+    private fun createUploadMediaWork(event: Event,
+                                      attachment: ContentAttachmentData,
+                                      isRoomEncrypted: Boolean,
+                                      startChain: Boolean = false): OneTimeWorkRequest {
         val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment, isRoomEncrypted)
         val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
 
         return matrixOneTimeWorkRequestBuilder<UploadContentWorker>()
                 .setConstraints(WorkManagerUtil.workConstraints)
+                .apply {
+                    if (startChain) {
+                        setInputMerger(NoMerger::class.java)
+                    }
+                }
                 .setInputData(uploadWorkData)
                 .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
                 .build()
     }
 
 }
+
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt
index 118fa7ccb5..e83181b89b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt
@@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResul
 import im.vector.matrix.android.internal.worker.SessionWorkerParams
 import im.vector.matrix.android.internal.worker.WorkerParamsFactory
 import im.vector.matrix.android.internal.worker.getSessionComponent
+import timber.log.Timber
 import java.util.concurrent.CountDownLatch
 import javax.inject.Inject
 
@@ -49,10 +50,13 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
     @Inject lateinit var localEchoUpdater: LocalEchoUpdater
 
     override fun doWork(): Result {
-
+        Timber.v("Start Encrypt work")
         val params = WorkerParamsFactory.fromData<Params>(inputData)
-                ?: return Result.success()
+                ?: return Result.success().also {
+                    Timber.v("Work cancelled due to input error from parent")
+                }
 
+        Timber.v("Start Encrypt work for event ${params.event.eventId}")
         if (params.lastFailureMessage != null) {
             // Transmit the error
             return Result.success(inputData)
@@ -97,7 +101,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
         latch.await()
 
         if (result != null) {
-            var modifiedContent = HashMap(result?.eventContent)
+            val modifiedContent = HashMap(result?.eventContent)
             params.keepKeys?.forEach { toKeep ->
                 localEvent.content?.get(toKeep)?.let {
                     //put it back in the encrypted thing
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/FakeSendWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/FakeSendWorker.kt
new file mode 100644
index 0000000000..f62c42d0c3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/FakeSendWorker.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.send
+
+import android.content.Context
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+
+internal class FakeSendWorker(context: Context, params: WorkerParameters)
+    : Worker(context, params) {
+
+    override fun doWork(): Result {
+        return Result.success()
+    }
+}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt
index 7f22fb2055..9d41979b3c 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoUpdater.kt
@@ -21,15 +21,21 @@ import im.vector.matrix.android.api.session.room.send.SendState
 import im.vector.matrix.android.internal.database.model.EventEntity
 import im.vector.matrix.android.internal.database.query.where
 import im.vector.matrix.android.internal.util.tryTransactionAsync
+import timber.log.Timber
 import javax.inject.Inject
 
 internal class LocalEchoUpdater @Inject constructor(private val monarchy: Monarchy) {
 
     fun updateSendState(eventId: String, sendState: SendState) {
+        Timber.v("Update local state of $eventId to ${sendState.name}")
         monarchy.tryTransactionAsync { realm ->
             val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
             if (sendingEventEntity != null) {
-                sendingEventEntity.sendState = sendState
+                if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) {
+                    //If already synced, do not put as sent
+                } else {
+                    sendingEventEntity.sendState = sendState
+                }
             }
         }
     }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/NoMerger.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/NoMerger.kt
new file mode 100644
index 0000000000..6938bc2258
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/NoMerger.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.send
+
+import androidx.work.Data
+import androidx.work.InputMerger
+
+class NoMerger : InputMerger() {
+    override fun merge(inputs: MutableList<Data>): Data {
+        return inputs.first()
+    }
+}
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt
index 05cd56e38f..ddc18e8771 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt
@@ -82,7 +82,10 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
                     Result.success()
                 }
             }
-        }, { Result.success() })
+        }, {
+            localEchoUpdater.updateSendState(event.eventId, SendState.SENT)
+            Result.success()
+        })
     }
 
 }
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt
index e1a8bdd746..70a5b12df2 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt
@@ -206,6 +206,23 @@ internal class DefaultTimeline(
         }
     }
 
+    override fun pendingEventCount(): Int {
+        var count = 0
+        Realm.getInstance(realmConfiguration).use {
+            count = RoomEntity.where(it,roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0
+        }
+        return count
+    }
+
+    override fun failedToDeliverEventCount(): Int {
+        var count = 0
+        Realm.getInstance(realmConfiguration).use {
+            count = RoomEntity.where(it,roomId).findFirst()?.sendingTimelineEvents?.filter {
+                it.root?.sendState?.hasFailed() ?: false
+            }?.count() ?: 0
+        }
+        return count
+    }
 
     override fun start() {
         if (isStarted.compareAndSet(false, true)) {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt
index 53906fdd76..e75bb91bf0 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt
@@ -51,9 +51,9 @@ internal object TimelineSendEventWorkCommon {
         }
     }
 
-    fun postWork(context: Context, roomId: String, workRequest: OneTimeWorkRequest) {
+    fun postWork(context: Context, roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND) {
         WorkManager.getInstance(context)
-                .beginUniqueWork(buildWorkIdentifier(roomId), ExistingWorkPolicy.APPEND, workRequest)
+                .beginUniqueWork(buildWorkIdentifier(roomId), policy, workRequest)
                 .enqueue()
     }
 
@@ -68,4 +68,8 @@ internal object TimelineSendEventWorkCommon {
     private fun buildWorkIdentifier(roomId: String): String {
         return "${roomId}_$SEND_WORK"
     }
+
+    fun cancelAllWorks(context: Context, roomId: String) {
+        WorkManager.getInstance(context).cancelUniqueWork(buildWorkIdentifier(roomId))
+    }
 }
\ No newline at end of file
diff --git a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
index 0d2c4cc409..1010c83bf2 100644
--- a/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
+++ b/matrix-sdk-android/src/main/res/values/strings_RiotX.xml
@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-
+    <string name="event_status_sending_message">Sending messageā€¦</string>
+    <string name="clear_timeline_send_queue">Clear sending queue</string>
 </resources>
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt
index db171300e6..58fcd0b5cd 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt
@@ -21,5 +21,5 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 
 fun TimelineEvent.canReact(): Boolean {
     // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
-    return root.getClearType() == EventType.MESSAGE && sendState.isSent() && !root.isRedacted()
+    return root.getClearType() == EventType.MESSAGE && root.sendState.isSent() && !root.isRedacted()
 }
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 ace0802e09..d25bf29f86 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
@@ -40,6 +40,10 @@ sealed class RoomDetailActions {
     data class EnterEditMode(val eventId: String) : RoomDetailActions()
     data class EnterQuoteMode(val eventId: String) : RoomDetailActions()
     data class EnterReplyMode(val eventId: String) : RoomDetailActions()
+    data class ResendMessage(val eventId: String) : RoomDetailActions()
+    data class RemoveFailedEcho(val eventId: String) : RoomDetailActions()
+    object ClearSendQueue : RoomDetailActions()
+    object ResendAll : RoomDetailActions()
 
 
 }
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt
index 6ad9a61f1a..be05e1a907 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt
@@ -19,7 +19,12 @@ package im.vector.riotx.features.home.room.detail
 import android.content.Context
 import android.content.Intent
 import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import com.airbnb.mvrx.activityViewModel
 import androidx.appcompat.widget.Toolbar
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProviders
 import im.vector.riotx.R
 import im.vector.riotx.core.extensions.replaceFragment
 import im.vector.riotx.core.platform.ToolbarConfigurable
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 cabb479575..36e4903c48 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
@@ -27,9 +27,7 @@ import android.os.Parcelable
 import android.text.Editable
 import android.text.Spannable
 import android.text.TextUtils
-import android.view.HapticFeedbackConstants
-import android.view.LayoutInflater
-import android.view.View
+import android.view.*
 import android.view.inputmethod.InputMethodManager
 import android.widget.TextView
 import android.widget.Toast
@@ -38,6 +36,7 @@ import androidx.appcompat.app.AlertDialog
 import androidx.core.app.ActivityOptionsCompat
 import androidx.core.content.ContextCompat
 import androidx.core.view.ViewCompat
+import androidx.core.view.forEach
 import androidx.lifecycle.ViewModelProviders
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.LinearLayoutManager
@@ -186,6 +185,8 @@ class RoomDetailFragment :
 
     override fun getLayoutResId() = R.layout.fragment_room_detail
 
+    override fun getMenuRes() = R.menu.menu_timeline
+
     private lateinit var actionViewModel: ActionsHandler
 
     @BindView(R.id.composerLayout)
@@ -239,6 +240,27 @@ class RoomDetailFragment :
         }
     }
 
+    override fun onPrepareOptionsMenu(menu: Menu) {
+        menu.forEach {
+            it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
+        }
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        if (item.itemId == R.id.clear_message_queue) {
+            //This a temporary option during dev as it is not super stable
+            //Cancel all pending actions in room queue and post a dummy
+            //Then mark all sending events as undelivered
+            roomDetailViewModel.process(RoomDetailActions.ClearSendQueue)
+            return true
+        }
+        if (item.itemId == R.id.resend_all) {
+            roomDetailViewModel.process(RoomDetailActions.ResendAll)
+            return true
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
     private fun exitSpecialMode() {
         commandAutocompletePolicy.enabled = true
         composerLayout.collapse()
@@ -805,6 +827,14 @@ class RoomDetailFragment :
                 showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
 
             }
+            MessageMenuViewModel.ACTION_RESEND         -> {
+                val eventId = actionData.data.toString()
+                roomDetailViewModel.process(RoomDetailActions.ResendMessage(eventId))
+            }
+            MessageMenuViewModel.ACTION_REMOVE         -> {
+                val eventId = actionData.data.toString()
+                roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(eventId))
+            }
             else                                       -> {
                 Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show()
             }
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 4b734c9557..95e0ca368f 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
@@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail
 
 import android.net.Uri
 import android.text.TextUtils
+import androidx.annotation.IdRes
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import com.airbnb.mvrx.FragmentViewModelContext
@@ -30,6 +31,8 @@ import com.squareup.inject.assisted.AssistedInject
 import im.vector.matrix.android.api.MatrixCallback
 import im.vector.matrix.android.api.session.Session
 import im.vector.matrix.android.api.session.content.ContentAttachmentData
+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.file.FileService
 import im.vector.matrix.android.api.session.room.model.Membership
@@ -40,6 +43,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 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
+import im.vector.riotx.R
 import im.vector.riotx.core.extensions.postLiveEvent
 import im.vector.riotx.core.intent.getFilenameFromUri
 import im.vector.riotx.core.platform.VectorViewModel
@@ -119,6 +123,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
             is RoomDetailActions.EnterReplyMode         -> handleReplyAction(action)
             is RoomDetailActions.DownloadFile           -> handleDownloadFile(action)
             is RoomDetailActions.NavigateToEvent        -> handleNavigateToEvent(action)
+            is RoomDetailActions.ResendMessage          -> handleResendEvent(action)
+            is RoomDetailActions.RemoveFailedEcho       -> handleRemove(action)
+            is RoomDetailActions.ClearSendQueue         -> handleClearSendQueue()
+            is RoomDetailActions.ResendAll              -> handleResendAll()
             else                                        -> Timber.e("Unhandled Action: $action")
         }
     }
@@ -157,6 +165,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         get() = _downloadedFileEvent
 
 
+    fun isMenuItemVisible(@IdRes itemId: Int): Boolean {
+        if (itemId == R.id.clear_message_queue) {
+            //For now always disable, woker cancellation is not working properly
+            return false//timeline.pendingEventCount() > 0
+        }
+        if (itemId == R.id.resend_all) {
+            return timeline.failedToDeliverEventCount() > 0
+        }
+        if (itemId == R.id.clear_all) {
+            return timeline.failedToDeliverEventCount() > 0
+        }
+        return false
+    }
+
     // PRIVATE METHODS *****************************************************************************
 
     private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
@@ -390,7 +412,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     }
 
     private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
-        if (action.event.sendState.isSent()) { //ignore pending/local events
+        if (action.event.root.sendState.isSent()) { //ignore pending/local events
             displayedEventsObservable.accept(action)
         }
         //We need to update this with the related m.replace also (to move read receipt)
@@ -524,6 +546,46 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     }
 
+    private fun handleResendEvent(action: RoomDetailActions.ResendMessage) {
+        val targetEventId = action.eventId
+        room.getTimeLineEvent(targetEventId)?.let {
+            //State must be UNDELIVERED or Failed
+            if (!it.root.sendState.hasFailed()) {
+                Timber.e("Cannot resend message, it is not failed, Cancel first")
+                return
+            }
+            if (it.root.isTextMessage()) {
+                room.resendTextMessage(it)
+            } else if (it.root.isImageMessage()) {
+                room.resendMediaMessage(it)
+            } else {
+                //TODO
+            }
+        }
+
+    }
+
+    private fun handleRemove(action: RoomDetailActions.RemoveFailedEcho) {
+        val targetEventId = action.eventId
+        room.getTimeLineEvent(targetEventId)?.let {
+            //State must be UNDELIVERED or Failed
+            if (!it.root.sendState.hasFailed()) {
+                Timber.e("Cannot resend message, it is not failed, Cancel first")
+                return
+            }
+            room.deleteFailedEcho(it)
+        }
+    }
+
+    private fun handleClearSendQueue() {
+        room.clearSendingQueue()
+    }
+
+    private fun handleResendAll() {
+        room.resendAllFailedMessages()
+    }
+
+
     private fun observeEventDisplayedActions() {
         // We are buffering scroll events for one second
         // and keep the most recent one to set the read receipt on.
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt
index cb283511ad..d30bad2f53 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomMessageTouchHelperCallback.kt
@@ -130,8 +130,7 @@ class RoomMessageTouchHelperCallback(private val context: Context,
 
 
     private fun drawReplyButton(canvas: Canvas, itemView: View) {
-
-        Timber.v("drawReplyButton")
+        //Timber.v("drawReplyButton")
         val translationX = Math.abs(itemView.translationX)
         val newTime = System.currentTimeMillis()
         val dt = Math.min(17, newTime - lastReplyButtonAnimationTime)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
index f8f5fe3eec..f3bec83a93 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt
@@ -138,6 +138,19 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
         }
         quickReactBottomDivider.isVisible = it.canReact()
         bottom_sheet_quick_reaction_container.isVisible = it.canReact()
+        if (it.informationData.sendState.isSending()) {
+            messageStatusInfo.isVisible = true
+            messageStatusProgress.isVisible = true
+            messageStatusText.text = getString(R.string.event_status_sending_message)
+            messageStatusText.setCompoundDrawables(null, null, null, null)
+        } else if (it.informationData.sendState.hasFailed()) {
+            messageStatusInfo.isVisible = true
+            messageStatusProgress.isVisible = false
+            messageStatusText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_warning_small, 0, 0, 0)
+            messageStatusText.text = getString(R.string.unable_to_send_message)
+        } else {
+            messageStatusInfo.isVisible = false
+        }
         return@withState
     }
 
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt
index 5b0dbdfed2..15e0883114 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt
@@ -20,6 +20,7 @@ import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
 import im.vector.matrix.android.api.session.Session
 import im.vector.matrix.android.api.session.events.model.EventType
+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.room.model.message.MessageContent
 import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
@@ -75,7 +76,9 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
         const val ACTION_REPLY = "reply"
         const val ACTION_SHARE = "share"
         const val ACTION_RESEND = "resend"
+        const val ACTION_REMOVE = "remove"
         const val ACTION_DELETE = "delete"
+        const val ACTION_CANCEL = "cancel"
         const val VIEW_SOURCE = "VIEW_SOURCE"
         const val VIEW_DECRYPTED_SOURCE = "VIEW_DECRYPTED_SOURCE"
         const val ACTION_COPY_PERMALINK = "ACTION_COPY_PERMALINK"
@@ -110,56 +113,57 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
                 ?: event.root.getClearContent().toModel()
         val type = messageContent?.type
 
-        val actions = if (!event.sendState.isSent()) {
-            //Resend and Delete
-            listOf<SimpleAction>(
-//                                SimpleAction(ACTION_RESEND, R.string.resend, R.drawable.ic_send, event.root.eventId),
-//                                //TODO delete icon
-//                                SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, event.root.eventId)
-            )
+        return if (event.root.sendState.hasFailed()) {
+            arrayListOf<SimpleAction>().apply {
+                if (canRetry(event)) {
+                    this.add(SimpleAction(ACTION_RESEND, R.string.global_retry, R.drawable.ic_refresh_cw, eventId))
+                }
+                this.add(SimpleAction(ACTION_REMOVE, R.string.remove, R.drawable.ic_trash, eventId))
+            }
+        } else if (event.root.sendState.isSending()) {
+            //TODO is uploading attachment?
+            arrayListOf<SimpleAction>().apply {
+                if (canCancel(event)) {
+                    this.add(SimpleAction(ACTION_CANCEL, R.string.cancel, R.drawable.ic_close_round, eventId))
+                }
+            }
         } else {
             arrayListOf<SimpleAction>().apply {
 
-                if (event.sendState == SendState.SENDING) {
-                    //TODO add cancel?
-                    return@apply
-                }
-                //TODO is downloading attachement?
-
                 if (!event.root.isRedacted()) {
 
                     if (canReply(event, messageContent)) {
-                        this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId))
+                        add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId))
                     }
 
                     if (canEdit(event, session.myUserId)) {
-                        this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId))
+                        add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId))
                     }
 
                     if (canRedact(event, session.myUserId)) {
-                        this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId))
+                        add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId))
                     }
 
                     if (canCopy(type)) {
                         //TODO copy images? html? see ClipBoard
-                        this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body))
+                        add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body))
                     }
 
                     if (event.canReact()) {
-                        this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId))
+                        add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId))
                     }
 
                     if (canQuote(event, messageContent)) {
-                        this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId))
+                        add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId))
                     }
 
                     if (canViewReactions(event)) {
-                        this.add(SimpleAction(ACTION_VIEW_REACTIONS, R.string.message_view_reaction, R.drawable.ic_view_reactions, informationData))
+                        add(SimpleAction(ACTION_VIEW_REACTIONS, R.string.message_view_reaction, R.drawable.ic_view_reactions, informationData))
                     }
 
                     if (canShare(type)) {
                         if (messageContent is MessageImageContent) {
-                            this.add(
+                            add(
                                     SimpleAction(ACTION_SHARE,
                                             R.string.share, R.drawable.ic_share,
                                             session.contentUrlResolver().resolveFullSize(messageContent.url))
@@ -169,7 +173,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
                     }
 
 
-                    if (event.sendState == SendState.SENT) {
+                    if (event.root.sendState == SendState.SENT) {
 
                         //TODO Can be redacted
 
@@ -177,23 +181,25 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
                     }
                 }
 
-                this.add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, event.root.toContentStringWithIndent()))
+                add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, event.root.toContentStringWithIndent()))
                 if (event.isEncrypted()) {
                     val decryptedContent = event.root.toClearContentStringWithIndent()
                             ?: stringProvider.getString(R.string.encryption_information_decryption_error)
-                    this.add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, decryptedContent))
+                    add(SimpleAction(VIEW_DECRYPTED_SOURCE, R.string.view_decrypted_source, R.drawable.ic_view_source, decryptedContent))
                 }
-                this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, event.root.eventId))
+                add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, event.root.eventId))
 
                 if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
                     //not sent by me
-                    this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, event.root.eventId))
+                    add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, event.root.eventId))
                 }
             }
         }
-        return actions
     }
 
+    private fun canCancel(event: TimelineEvent): Boolean {
+        return false
+    }
 
     private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
         //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
@@ -232,6 +238,11 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
         return event.root.senderId == myUserId
     }
 
+    private fun canRetry(event: TimelineEvent): Boolean {
+        return event.root.sendState.hasFailed() && event.root.isTextMessage()
+    }
+
+
     private fun canViewReactions(event: TimelineEvent): Boolean {
         //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
         if (event.root.getClearType() != EventType.MESSAGE) return false
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
index ea7036b741..4a3f50c45e 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
@@ -43,7 +43,7 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
         val informationData = MessageInformationData(
                 eventId = event.root.eventId ?: "?",
                 senderId = event.root.senderId ?: "",
-                sendState = event.sendState,
+                sendState = event.root.sendState,
                 avatarUrl = event.senderAvatar(),
                 memberName = event.senderName(),
                 showInformation = false
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index b2e8216b5f..c9da3ce6d0 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -97,7 +97,7 @@ class MessageItemFactory @Inject constructor(
                 val informationData = MessageInformationData(
                         eventId = event.root.eventId ?: "?",
                         senderId = event.root.senderId ?: "",
-                        sendState = event.sendState,
+                        sendState = event.root.sendState,
                         time = "",
                         avatarUrl = event.senderAvatar(),
                         memberName = "",
@@ -121,7 +121,7 @@ class MessageItemFactory @Inject constructor(
                     event.annotations?.editSummary,
                     highlight,
                     callback)
-            is MessageTextContent   -> buildTextMessageItem(event.sendState,
+            is MessageTextContent   -> buildTextMessageItem(event.root.sendState,
                     messageContent,
                     informationData,
                     event.annotations?.editSummary,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt
index c23fdfbd7b..52771ad6e9 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/NoticeItemFactory.kt
@@ -37,7 +37,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
         val informationData = MessageInformationData(
                 eventId = event.root.eventId ?: "?",
                 senderId = event.root.senderId ?: "",
-                sendState = event.sendState,
+                sendState = event.root.sendState,
                 avatarUrl = event.senderAvatar(),
                 memberName = event.senderName(),
                 showInformation = false
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index 4a927b1979..c6ddab19d9 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -74,7 +74,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
                     val informationData = MessageInformationData(
                             eventId = event.root.eventId ?: "?",
                             senderId = event.root.senderId ?: "",
-                            sendState = event.sendState,
+                            sendState = event.root.sendState,
                             time = "",
                             avatarUrl = event.senderAvatar(),
                             memberName = "",
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
index 6f7f5b86b2..fad2587087 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
@@ -162,10 +162,15 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
         return true
     }
 
-    protected fun renderSendState(root: View, textView: TextView?) {
+    protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) {
         root.isClickable = informationData.sendState.isSent()
         val state = if (informationData.hasPendingEdits) SendState.UNSENT else informationData.sendState
         textView?.setTextColor(colorProvider.getMessageTextColor(state))
+        failureIndicator?.isVisible = when (informationData.sendState) {
+            SendState.UNDELIVERED,
+            SendState.FAILED_UNKNOWN_DEVICES -> true
+            else                             -> false
+        }
     }
 
     abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt
index 6a68557f86..9ed9103c41 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageImageVideoItem.kt
@@ -43,14 +43,20 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
     override fun bind(holder: Holder) {
         super.bind(holder)
         imageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView)
-        contentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
+        if (!informationData.sendState.hasFailed()) {
+            contentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
+        }
         holder.imageView.setOnClickListener(clickListener)
         holder.imageView.setOnLongClickListener(longClickListener)
         ViewCompat.setTransitionName(holder.imageView,"imagePreview_${id()}")
         holder.mediaContentView.setOnClickListener(cellClickListener)
         holder.mediaContentView.setOnLongClickListener(longClickListener)
         // The sending state color will be apply to the progress text
-        renderSendState(holder.imageView, null)
+        renderSendState(holder.imageView, null, holder.failedToSendIndicator)
+        holder.progressLayout
+        if (informationData.sendState.hasFailed()) {
+
+        }
         holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
     }
 
@@ -67,6 +73,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
         val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
 
         val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia)
+        val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator)
     }
 
     companion object {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt
index eb13ac7b33..5cd873ff10 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/util/MessageInformationDataFactory.kt
@@ -64,7 +64,7 @@ class MessageInformationDataFactory @Inject constructor(private val timelineDate
         return MessageInformationData(
                 eventId = eventId,
                 senderId = event.root.senderId ?: "",
-                sendState = event.sendState,
+                sendState = event.root.sendState,
                 time = time,
                 avatarUrl = avatarUrl,
                 memberName = formattedMemberName,
diff --git a/vector/src/main/res/drawable/ic_refresh_cw.xml b/vector/src/main/res/drawable/ic_refresh_cw.xml
new file mode 100644
index 0000000000..72c8bd573f
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_refresh_cw.xml
@@ -0,0 +1,22 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="22dp"
+    android:height="19dp"
+    android:viewportWidth="22"
+    android:viewportHeight="19">
+  <path
+      android:pathData="M21,2.741v5.333h-5.455M1,16.963V11.63h5.455"
+      android:strokeLineJoin="round"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:fillType="evenOdd"
+      android:strokeColor="#9E9E9E"
+      android:strokeLineCap="round"/>
+  <path
+      android:pathData="M3.282,7.185c0.937,-2.589 3.167,-4.527 5.907,-5.133 2.74,-0.607 5.607,0.204 7.593,2.147L21,8.074M1,11.63l4.218,3.875c1.986,1.943 4.853,2.754 7.593,2.148 2.74,-0.606 4.97,-2.545 5.907,-5.134"
+      android:strokeLineJoin="round"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:fillType="evenOdd"
+      android:strokeColor="#9E9E9E"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_trash.xml b/vector/src/main/res/drawable/ic_trash.xml
new file mode 100644
index 0000000000..0be5f42dbb
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_trash.xml
@@ -0,0 +1,14 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="23dp"
+    android:viewportWidth="20"
+    android:viewportHeight="23">
+  <path
+      android:pathData="M1,5.852h18M17,5.852v14a2,2 0,0 1,-2 2H5a2,2 0,0 1,-2 -2v-14m3,0v-2a2,2 0,0 1,2 -2h4a2,2 0,0 1,2 2v2M8,10.852v6M12,10.852v6"
+      android:strokeLineJoin="round"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:fillType="evenOdd"
+      android:strokeColor="#9E9E9E"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/vector/src/main/res/drawable/ic_warning_small.xml b/vector/src/main/res/drawable/ic_warning_small.xml
new file mode 100644
index 0000000000..456491ec82
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_warning_small.xml
@@ -0,0 +1,14 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="14dp"
+    android:height="14dp"
+    android:viewportWidth="14"
+    android:viewportHeight="14">
+  <path
+      android:pathData="M7,12.852A6,6 0,1 0,7 0.852a6,6 0,0 0,0 12zM7,1.452a1.8,1.8 0,0 1,1.8 1.8L8.8,6.852a1.8,1.8 0,1 1,-3.6 0L5.2,3.252A1.8,1.8 0,0 1,7 1.452zM7,12.252a1.8,1.8 0,1 0,0 -3.6,1.8 1.8,0 0,0 0,3.6z"
+      android:strokeLineJoin="round"
+      android:strokeWidth="1.44"
+      android:fillColor="#FF4B55"
+      android:fillType="evenOdd"
+      android:strokeColor="#FF4B55"
+      android:strokeLineCap="round"/>
+</vector>
diff --git a/vector/src/main/res/layout/bottom_sheet_message_actions.xml b/vector/src/main/res/layout/bottom_sheet_message_actions.xml
index 9fadcee16f..c7d4f5ac8e 100644
--- a/vector/src/main/res/layout/bottom_sheet_message_actions.xml
+++ b/vector/src/main/res/layout/bottom_sheet_message_actions.xml
@@ -87,6 +87,38 @@
                 tools:text="Friday 8pm" />
         </androidx.constraintlayout.widget.ConstraintLayout>
 
+        <LinearLayout
+            android:id="@+id/messageStatusInfo"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:layout_marginBottom="4dp"
+            android:layout_marginEnd="16dp">
+
+            <ProgressBar
+                android:id="@+id/messageStatusProgress"
+                style="?android:attr/progressBarStyleSmall"
+                android:layout_width="12dp"
+                android:layout_height="12dp"
+                android:layout_gravity="center"
+                android:layout_marginEnd="4dp"
+                android:visibility="gone"
+                tools:visibility="visible" />
+
+            <TextView
+                android:id="@+id/messageStatusText"
+                android:textColor="?riotx_text_secondary"
+                android:textStyle="bold"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginEnd="16dp"
+                android:layout_weight="1"
+                android:drawableStart="@drawable/ic_warning_small"
+                android:drawablePadding="4dp"
+                tools:text="@string/unable_to_send_message" />
+
+        </LinearLayout>
+
         <View
             android:id="@+id/quickReactTopDivider"
             android:layout_width="match_parent"
diff --git a/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml b/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml
index 5ea117e323..8fe373790d 100644
--- a/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml
@@ -15,7 +15,19 @@
         app:layout_constraintHorizontal_bias="0"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent"
-        tools:layout_height="300dp" />
+        tools:layout_height="300dp"
+        tools:src="@tools:sample/backgrounds/scenic" />
+
+    <ImageView
+        android:id="@+id/messageFailToSendIndicator"
+        android:layout_width="14dp"
+        android:layout_height="14dp"
+        android:layout_marginStart="2dp"
+        android:src="@drawable/ic_warning_small"
+        android:visibility="gone"
+        app:layout_constraintStart_toEndOf="@id/messageThumbnailView"
+        app:layout_constraintTop_toTopOf="@id/messageThumbnailView"
+        tools:visibility="visible" />
 
     <ImageView
         android:id="@+id/messageMediaPlayView"
diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml
new file mode 100644
index 0000000000..824735406f
--- /dev/null
+++ b/vector/src/main/res/menu/menu_timeline.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <item
+        android:id="@+id/resend_all"
+        android:icon="@drawable/ic_refresh_cw"
+        android:title="@string/room_prompt_resend"
+        android:visible="false"
+        app:showAsAction="never"
+        tools:visible="true" />
+
+    <item
+        android:id="@+id/clear_all"
+        android:icon="@drawable/ic_trash"
+        android:title="@string/room_prompt_cancel"
+        android:visible="false"
+        app:showAsAction="never"
+        tools:visible="true" />
+
+    <item
+        android:id="@+id/clear_message_queue"
+        android:title="@string/clear_timeline_send_queue"
+        android:visible="false"
+        app:showAsAction="never"
+        tools:visible="true" />
+
+</menu>
\ No newline at end of file
diff --git a/vector/src/main/res/menu/vector_room_message_settings.xml b/vector/src/main/res/menu/vector_room_message_settings.xml
deleted file mode 100755
index 7532fe9dc5..0000000000
--- a/vector/src/main/res/menu/vector_room_message_settings.xml
+++ /dev/null
@@ -1,89 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <item
-        android:id="@+id/ic_action_vector_resend_message"
-        android:icon="@drawable/ic_material_send_black"
-        android:title="@string/resend"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_vector_cancel_upload"
-        android:icon="@drawable/vector_cancel_upload_download"
-        android:title="@string/room_event_action_cancel_upload"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_vector_cancel_download"
-        android:icon="@drawable/vector_cancel_upload_download"
-        android:title="@string/room_event_action_cancel_download"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_vector_redact_message"
-        android:icon="@drawable/ic_material_delete"
-        android:title="@string/redact"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_vector_copy"
-        android:icon="@drawable/ic_material_copy"
-        android:title="@string/copy"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_vector_quote"
-        android:icon="@drawable/ic_material_quote"
-        android:title="@string/quote"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_vector_share"
-        android:icon="@drawable/ic_material_share"
-        android:title="@string/share"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_vector_forward"
-        android:icon="@drawable/ic_material_forward"
-        android:title="@string/forward"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_vector_save"
-        android:icon="@drawable/ic_material_save"
-        android:title="@string/save"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_view_source"
-        android:icon="@drawable/ic_material_message_black"
-        android:title="@string/view_source"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_view_decrypted_source"
-        android:icon="@drawable/ic_material_message_black"
-        android:title="@string/view_decrypted_source"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_vector_permalink"
-        android:icon="@drawable/ic_material_link_black"
-        android:title="@string/permalink"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_vector_report"
-        android:icon="@drawable/ic_report_black"
-        android:title="@string/report_content"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/ic_action_device_verification"
-        android:icon="@drawable/ic_perm_device_information_black"
-        android:title="@string/device_information"
-        app:showAsAction="never" />
-
-</menu>
\ No newline at end of file
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index daab4259e8..f5d11432d0 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -507,7 +507,7 @@
     <string name="room_unsent_messages_notification">Messages not sent. %1$s or %2$s now?</string>
     <string name="room_unknown_devices_messages_notification">Messages not sent due to unknown devices being present. %1$s or %2$s now?</string>
     <string name="room_prompt_resend">Resend all</string>
-    <string name="room_prompt_cancel">cancel all</string>
+    <string name="room_prompt_cancel">Cancel all</string>
     <string name="room_resend_unsent_messages">Resend unsent messages</string>
     <string name="room_delete_unsent_messages">Delete unsent messages</string>
     <string name="room_message_file_not_found">File not found</string>