diff --git a/CHANGES.md b/CHANGES.md index de52836bce..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: - @@ -15,6 +16,7 @@ Bugfix: - Edited message: link confusion when (edited) appears in body (#398) - Close detail room screen when the room is left with another client (#256) - Clear notification for a room left on another client + - Fix messages with empty `in_reply_to` not rendering (#447) Translations: - 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()?.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/model/message/MessageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt index bd32a75a47..c116c6b315 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt @@ -29,5 +29,5 @@ interface MessageContent { fun MessageContent?.isReply(): Boolean { - return this?.relatesTo?.inReplyTo != null -} \ No newline at end of file + return this?.relatesTo?.inReplyTo?.eventId != null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt index 3df8a534a5..9ed629acda 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt @@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ReplyToContent( - @Json(name = "event_id") val eventId: String -) \ No newline at end of file + @Json(name = "event_id") val eventId: String? = null +) 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(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/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index a65c466a68..dda8b9322f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -62,9 +62,7 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C roomId: String, membership: Membership? = null, roomSummary: RoomSyncSummary? = null, - unreadNotifications: RoomSyncUnreadNotifications? = null, - isDirect: Boolean? = null, - directUserId: String? = null) { + unreadNotifications: RoomSyncUnreadNotifications? = null) { val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) @@ -97,10 +95,6 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C .asSequence() .map { it.stateKey } - if (isDirect != null) { - roomSummaryEntity.isDirect = isDirect - roomSummaryEntity.directUserId = directUserId - } roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.topic = lastTopicEvent?.content.toModel()?.topic diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt index 6091f6b96c..04982c9a33 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/create/CreateRoomTask.kt @@ -86,7 +86,7 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro this.isDirect = true } }.flatMap { - val directChats = directChatsHelper.getDirectChats() + val directChats = directChatsHelper.getLocalUserAccount() updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats)) }.flatMap { Try.just(roomId) 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() ?: 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 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(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 { + 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/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 1249bc4f4b..ae20fa683e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -35,14 +35,10 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.find import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom -import im.vector.matrix.android.internal.database.query.isDirect import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService -import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper -import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.session.mapWithProgress import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask @@ -70,9 +66,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch private val tokenStore: SyncTokenStore, private val pushRuleService: DefaultPushRuleService, private val processForPushTask: ProcessEventForPushTask, - private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val credentials: Credentials, - private val directChatsHelper: DirectChatsHelper, private val taskExecutor: TaskExecutor) { sealed class HandlingStrategy { @@ -192,21 +186,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events) roomEntity.addOrUpdate(chunkEntity) } - val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(credentials.userId) - val inviterId = myUserStateEvent?.sender - val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() } - val isDirect = myUserRoomMember?.isDirect - if (isDirect == true && inviterId != null) { - val isAlreadyDirect = RoomSummaryEntity.isDirect(realm, roomId) - if (!isAlreadyDirect) { - val directChatsMap = directChatsHelper.getDirectChats(include = Pair(inviterId, roomId)) - val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams( - directMessages = directChatsMap - ) - updateUserAccountDataTask.configureWith(updateUserAccountParams).executeBy(taskExecutor) - } - } - roomSummaryUpdater.update(realm, roomId, Membership.INVITE, isDirect = isDirect, directUserId = inviterId) + roomSummaryUpdater.update(realm, roomId, Membership.INVITE) return roomEntity } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt index d680a3186a..b49b787f50 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt @@ -17,11 +17,23 @@ package im.vector.matrix.android.internal.session.sync import arrow.core.Try +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.R +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.crypto.CryptoManager +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.query.isDirect import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.reportSubtask +import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.sync.model.SyncResponse +import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith import timber.log.Timber import javax.inject.Inject import kotlin.system.measureTimeMillis @@ -88,9 +100,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl measureTimeMillis { reportSubtask(reporter, R.string.initial_sync_start_importing_account_data, 100, 0.1f) { Timber.v("Handle accountData") - if (syncResponse.accountData != null) { - userAccountDataSyncHandler.handle(syncResponse.accountData) - } + userAccountDataSyncHandler.handle(syncResponse.accountData, syncResponse.rooms?.invite) } }.also { Timber.v("Finish handling accountData in $it ms") @@ -98,7 +108,6 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl Timber.v("On sync completed") cryptoSyncHandler.onSyncCompleted(syncResponse) - } Timber.v("Finish handling sync in $measure ms") syncResponse diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index e0be3b14eb..6ea4693168 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -17,28 +17,45 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.RoomSummaryEntity -import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.query.getDirectRooms import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages import im.vector.matrix.android.internal.session.sync.model.UserAccountDataSync +import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper +import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import io.realm.Realm +import timber.log.Timber import javax.inject.Inject -internal class UserAccountDataSyncHandler @Inject constructor(private val monarchy: Monarchy) { +internal class UserAccountDataSyncHandler @Inject constructor(private val monarchy: Monarchy, + private val credentials: Credentials, + private val directChatsHelper: DirectChatsHelper, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val taskExecutor: TaskExecutor) { - fun handle(accountData: UserAccountDataSync) { - accountData.list.forEach { + fun handle(accountData: UserAccountDataSync?, invites: Map?) { + accountData?.list?.forEach { when (it) { is UserAccountDataDirectMessages -> handleDirectChatRooms(it) else -> return@forEach } } + monarchy.doWithRealm { realm -> + synchronizeWithServerIfNeeded(realm, invites) + } } private fun handleDirectChatRooms(directMessages: UserAccountDataDirectMessages) { monarchy.runTransactionSync { realm -> - val oldDirectRooms = RoomSummaryEntity.getDirectRooms(realm) oldDirectRooms.forEach { it.isDirect = false @@ -57,4 +74,35 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc } } } + + // If we get some direct chat invites, we synchronize the user account data including those. + private fun synchronizeWithServerIfNeeded(realm: Realm, invites: Map?) { + if (invites.isNullOrEmpty()) return + val directChats = directChatsHelper.getLocalUserAccount() + var hasUpdate = false + invites.forEach { (roomId, _) -> + val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(credentials.userId) + val inviterId = myUserStateEvent?.sender + val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() } + val isDirect = myUserRoomMember?.isDirect + if (inviterId != null && inviterId != credentials.userId && isDirect == true) { + directChats + .getOrPut(inviterId, { arrayListOf() }) + .apply { + if (contains(roomId)) { + Timber.v("Direct chats already include room $roomId with user $inviterId") + } else { + add(roomId) + hasUpdate = true + } + } + } + } + if (hasUpdate) { + val updateUserAccountParams = UpdateUserAccountDataTask.DirectChatParams( + directMessages = directChats + ) + updateUserAccountDataTask.configureWith(updateUserAccountParams).executeBy(taskExecutor) + } + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt index 5d135b7bd5..8d23a988cd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DirectChatsHelper.kt @@ -24,27 +24,24 @@ import io.realm.RealmConfiguration import timber.log.Timber import javax.inject.Inject -internal class DirectChatsHelper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { +internal class DirectChatsHelper @Inject constructor(@SessionDatabase + private val realmConfiguration: RealmConfiguration) { - fun getDirectChats(include: Pair? = null, filterRoomId: String? = null): Map> { + /** + * @return a map of userId <-> list of roomId + */ + fun getLocalUserAccount(filterRoomId: String? = null): MutableMap> { return Realm.getInstance(realmConfiguration).use { realm -> val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm) val directChatsMap = mutableMapOf>() for (directRoom in currentDirectRooms) { if (directRoom.roomId == filterRoomId) continue val directUserId = directRoom.directUserId ?: continue - directChatsMap.getOrPut(directUserId, { arrayListOf() }).apply { - add(directRoom.roomId) - } - } - if (include != null) { - directChatsMap.getOrPut(include.first, { arrayListOf() }).apply { - if (contains(include.second)) { - Timber.v("Direct chats already include room ${include.second} with user ${include.first}") - } else { - add(include.second) - } - } + directChatsMap + .getOrPut(directUserId, { arrayListOf() }) + .apply { + add(directRoom.roomId) + } } directChatsMap } 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 @@ - + 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 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 79b00b6911..e60bc422a8 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 @@ -42,6 +42,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 80e943cb04..e1731c27ef 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 99ae9eac67..8d4262c7f9 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 @@ -188,6 +187,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) @@ -271,6 +272,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() @@ -874,6 +896,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 d4732ad7f1..2bb7327d57 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 @@ -32,6 +33,8 @@ import im.vector.matrix.android.api.MatrixPatterns 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.EventType +import im.vector.matrix.android.api.session.events.model.isImageMessage +import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.model.Membership @@ -43,6 +46,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 @@ -123,6 +127,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailActions.DownloadFile -> handleDownloadFile(action) is RoomDetailActions.NavigateToEvent -> handleNavigateToEvent(action) is RoomDetailActions.HandleTombstoneEvent -> handleTombstoneEvent(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") } } @@ -186,6 +194,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) { @@ -419,7 +441,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) @@ -553,6 +575,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(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 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 a3b2250912..43197d8bad 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 @@ -76,7 +76,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 : 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 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(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 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 @@ + + + + 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 @@ + + + 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 @@ + + + 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" /> + + + + + + + + @@ -113,7 +111,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:layout_marginLeft="16dp" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:minHeight="@dimen/layout_touch_size" 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" /> + + + + + + + + + + + \ 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 @@ 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