From 000db4b19231566e1989894cfacaeeea528fd251 Mon Sep 17 00:00:00 2001 From: Valere 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 5de98507..a85c2ff6 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 547e627f..7dd0c6e2 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()?.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()?.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 94abd5d3..ae276adb 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 75e3c0f6..e9f22da4 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 e52ac3b4..314c9f61 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 044aa957..ef2769d9 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 30346f78..0d76b548 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 92cbd4be..fa067999 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 8e1a0281..b015670d 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(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 24dc14a7..ab373a6e 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 9a94b05b..8adb45dd 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() ?: return null +// when (messageContent.type) { +// MessageType.MSGTYPE_IMAGE -> { +// val imageContent = clearContent.toModel() ?: 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() + .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() + 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() .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(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() .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 118fa7cc..e83181b8 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(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 00000000..f62c42d0 --- /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 7f22fb20..9d41979b 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 00000000..6938bc22 --- /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 { + 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 05cd56e3..ddc18e87 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 e1a8bdd7..70a5b12d 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 53906fdd..e75bb91b 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 0d2c4cc4..1010c83b 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 @@ - + Sending messageā€¦ + Clear sending queue \ 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 db171300..58fcd0b5 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 ace0802e..d25bf29f 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 6ad9a61f..be05e1a9 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 cabb4795..36e4903c 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 4b734c95..95e0ca36 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 cb283511..d30bad2f 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 f8f5fe3e..f3bec83a 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 5b0dbdfe..15e08831 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(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().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().apply { + if (canCancel(event)) { + this.add(SimpleAction(ACTION_CANCEL, R.string.cancel, R.drawable.ic_close_round, eventId)) + } + } } else { arrayListOf().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 ea7036b7..4a3f50c4 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 b2e8216b..c9da3ce6 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 c23fdfbd..52771ad6 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 4a927b19..c6ddab19 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 6f7f5b86..fad25870 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 : BaseEventItem() { 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 6a68557f..9ed9103c 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(R.id.messageMediaPlayView) val mediaContentView by bind(R.id.messageContentMedia) + val failedToSendIndicator by bind(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 eb13ac7b..5cd873ff 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 00000000..72c8bd57 --- /dev/null +++ b/vector/src/main/res/drawable/ic_refresh_cw.xml @@ -0,0 +1,22 @@ + + + + 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 00000000..0be5f42d --- /dev/null +++ b/vector/src/main/res/drawable/ic_trash.xml @@ -0,0 +1,14 @@ + + + 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 00000000..456491ec --- /dev/null +++ b/vector/src/main/res/drawable/ic_warning_small.xml @@ -0,0 +1,14 @@ + + + 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 9fadcee1..c7d4f5ac 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" /> + + + + + + + + + tools:layout_height="300dp" + tools:src="@tools:sample/backgrounds/scenic" /> + + + + + + + + + + + \ 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 7532fe9d..00000000 --- a/vector/src/main/res/menu/vector_room_message_settings.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 daab4259..f5d11432 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -507,7 +507,7 @@ Messages not sent. %1$s or %2$s now? Messages not sent due to unknown devices being present. %1$s or %2$s now? Resend all - cancel all + Cancel all Resend unsent messages Delete unsent messages File not found