From c178535cc860c9f00356216e06338fef5b0d5640 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 25 Feb 2021 13:32:56 +0300 Subject: [PATCH] Show send status of text messages. --- .../timeline/TimelineEventController.kt | 4 ++- .../timeline/factory/CallItemFactory.kt | 2 +- .../timeline/factory/DefaultItemFactory.kt | 2 +- .../timeline/factory/EncryptedItemFactory.kt | 3 ++- .../timeline/factory/EncryptionItemFactory.kt | 2 +- .../timeline/factory/MessageItemFactory.kt | 3 ++- .../timeline/factory/NoticeItemFactory.kt | 2 +- .../timeline/factory/TimelineItemFactory.kt | 7 ++--- .../factory/VerificationItemFactory.kt | 4 +-- .../timeline/factory/WidgetItemFactory.kt | 2 +- .../helper/MessageInformationDataFactory.kt | 27 ++++++++++++++++--- .../detail/timeline/item/AbsMessageItem.kt | 24 ++++++++++++++++- .../timeline/item/MessageInformationData.kt | 10 ++++++- .../drawable/ic_delete_unsent_messages.xml | 9 +++++++ .../src/main/res/drawable/ic_message_sent.xml | 13 +++++++++ .../drawable/ic_retry_sending_messages.xml | 10 +++++++ .../main/res/drawable/ic_sending_message.xml | 10 +++++++ .../drawable/ic_sending_message_failed.xml | 10 +++++++ .../res/layout/item_timeline_event_base.xml | 12 +++++++++ 19 files changed, 138 insertions(+), 18 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_delete_unsent_messages.xml create mode 100644 vector/src/main/res/drawable/ic_message_sent.xml create mode 100644 vector/src/main/res/drawable/ic_retry_sending_messages.xml create mode 100644 vector/src/main/res/drawable/ic_sending_message.xml create mode 100644 vector/src/main/res/drawable/ic_sending_message_failed.xml diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index d7295176fe..7a1b758f3a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -30,6 +30,7 @@ import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull +import im.vector.app.core.extensions.prevOrNull import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState @@ -336,11 +337,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun buildCacheItem(currentPosition: Int, items: List): CacheItemData { val event = items[currentPosition] val nextEvent = items.nextOrNull(currentPosition) + val prevEvent = items.prevOrNull(currentPosition) if (hasReachedInvite && hasUTD) { return CacheItemData(event.localId, event.root.eventId, null, null, null) } updateUTDStates(event, nextEvent) - val eventModel = timelineItemFactory.create(event, nextEvent, eventIdToHighlight, callback).also { + val eventModel = timelineItemFactory.create(event, prevEvent, nextEvent, eventIdToHighlight, callback).also { it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt index d3dd94eae7..548f7a3b1c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -52,7 +52,7 @@ class CallItemFactory @Inject constructor( ): VectorEpoxyModel<*>? { if (event.root.eventId == null) return null val roomId = event.roomId - val informationData = messageInformationDataFactory.create(event, null) + val informationData = messageInformationDataFactory.create(event, null, null) val callSignalingContent = event.getCallSignallingContent() ?: return null val callId = callSignalingContent.callId ?: return null val call = callManager.getCallById(callId) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt index 9d82103d3b..71ac46307b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt @@ -61,7 +61,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava } else { stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId) } - val informationData = informationDataFactory.create(event, null) + val informationData = informationDataFactory.create(event, null, null) return create(text, informationData, highlight, callback) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index e88c1f3797..b531e08359 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -47,6 +47,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat private val vectorPreferences: VectorPreferences) { fun create(event: TimelineEvent, + prevEvent: TimelineEvent?, nextEvent: TimelineEvent?, highlight: Boolean, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { @@ -108,7 +109,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat } } - val informationData = messageInformationDataFactory.create(event, nextEvent) + val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent) val attributes = attributesFactory.create(event.root.content.toModel(), informationData, callback) return MessageTextItem_() .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 1eb09f2e7a..68716a3eba 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -48,7 +48,7 @@ class EncryptionItemFactory @Inject constructor( return null } val algorithm = event.root.getClearContent().toModel()?.algorithm - val informationData = informationDataFactory.create(event, null) + val informationData = informationDataFactory.create(event, null, null) val attributes = messageItemAttributesFactory.create(null, informationData, callback) val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 2e97abc32e..e969998613 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -119,13 +119,14 @@ class MessageItemFactory @Inject constructor( } fun create(event: TimelineEvent, + prevEvent: TimelineEvent?, nextEvent: TimelineEvent?, highlight: Boolean, callback: TimelineEventController.Callback? ): VectorEpoxyModel<*>? { event.root.eventId ?: return null roomId = event.roomId - val informationData = messageInformationDataFactory.create(event, nextEvent) + val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent) if (event.root.isRedacted()) { // message is redacted val attributes = messageItemAttributesFactory.create(null, informationData, callback) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index 12c7c2318a..dfabf96199 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -35,7 +35,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv highlight: Boolean, callback: TimelineEventController.Callback?): NoticeItem? { val formattedText = eventFormatter.format(event) ?: return null - val informationData = informationDataFactory.create(event, null) + val informationData = informationDataFactory.create(event, null, null) val attributes = NoticeItem.Attributes( avatarRenderer = avatarRenderer, informationData = informationData, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 7fd50147d4..f1df2ae802 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -38,6 +38,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me private val userPreferencesProvider: UserPreferencesProvider) { fun create(event: TimelineEvent, + prevEvent: TimelineEvent?, nextEvent: TimelineEvent?, eventIdToHighlight: String?, callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { @@ -46,7 +47,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me val computedModel = try { when (event.root.getClearType()) { EventType.STICKER, - EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback) + EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback) // State and call EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, @@ -76,9 +77,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.ENCRYPTED -> { if (event.root.isRedacted()) { // Redacted event, let the MessageItemFactory handle it - messageItemFactory.create(event, nextEvent, highlight, callback) + messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback) } else { - encryptedItemFactory.create(event, nextEvent, highlight, callback) + encryptedItemFactory.create(event, prevEvent, nextEvent, highlight, callback) } } EventType.STATE_ROOM_ALIASES, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt index eb539d2b8a..960487140d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -75,9 +75,9 @@ class VerificationItemFactory @Inject constructor( // If it's not a request ignore this event // if (refEvent.root.getClearContent().toModel() == null) return ignoredConclusion(event, highlight, callback) - val referenceInformationData = messageInformationDataFactory.create(refEvent, null) + val referenceInformationData = messageInformationDataFactory.create(refEvent, null, null) - val informationData = messageInformationDataFactory.create(event, null) + val informationData = messageInformationDataFactory.create(event, null, null) val attributes = messageItemAttributesFactory.create(null, informationData, callback) when (event.root.getClearType()) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt index 260958b19e..a6a88a3444 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt @@ -64,7 +64,7 @@ class WidgetItemFactory @Inject constructor( callback: TimelineEventController.Callback?, widgetContent: WidgetContent, previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> { - val informationData = informationDataFactory.create(timelineEvent, null) + val informationData = informationDataFactory.create(timelineEvent, null, null) val attributes = messageItemAttributesFactory.create(null, informationData, callback) val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 951a4d3fa0..2f71c555f9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -25,6 +25,7 @@ import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData +import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.crypto.VerificationState import org.matrix.android.sdk.api.extensions.orFalse @@ -49,7 +50,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses private val dateFormatter: VectorDateFormatter, private val vectorPreferences: VectorPreferences) { - fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { + fun create(event: TimelineEvent, prevEvent: TimelineEvent?, nextEvent: TimelineEvent?): MessageInformationData { // Non nullability has been tested before val eventId = event.root.eventId!! @@ -70,6 +71,13 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) val e2eDecoration = getE2EDecoration(event) + val isSentByMe = event.root.senderId == session.myUserId + val sendStateDecoration = if (isSentByMe) { + getSendStateDecoration(event.root.sendState, prevEvent?.root?.sendState, event.readReceipts.any { it.user.userId != session.myUserId }) + } else { + SendStateDecoration.NONE + } + return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", @@ -110,11 +118,24 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ?: VerificationState.REQUEST ReferencesInfoData(verificationState) }, - sentByMe = event.root.senderId == session.myUserId, - e2eDecoration = e2eDecoration + sentByMe = isSentByMe, + e2eDecoration = e2eDecoration, + sendStateDecoration = sendStateDecoration ) } + private fun getSendStateDecoration(eventSendState: SendState, prevEventSendState: SendState?, anyReadReceipts: Boolean): SendStateDecoration { + return if (eventSendState.isSending()) { + SendStateDecoration.SENDING + } else if (eventSendState.hasFailed()) { + SendStateDecoration.FAILED + } else if (eventSendState.isSent() && !prevEventSendState?.isSent().orFalse() && !anyReadReceipts) { + SendStateDecoration.SENT + } else { + SendStateDecoration.NONE + } + } + private fun getE2EDecoration(event: TimelineEvent): E2EDecoration { val roomSummary = roomSummariesHolder.get(event.roomId) return if ( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index d4b1b8859a..9f73a504ac 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -31,7 +31,7 @@ import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController /** - * Base timeline item that adds an optional information bar with the sender avatar, name and time + * Base timeline item that adds an optional information bar with the sender avatar, name, time, send state * Adds associated click listeners (on avatar, displayname) */ abstract class AbsMessageItem : AbsBaseMessageItem() { @@ -82,6 +82,27 @@ abstract class AbsMessageItem : AbsBaseMessageItem holder.avatarImageView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null) } + + // Render send state indicator + holder.sendStateImageView.isVisible = true + when (attributes.informationData.sendStateDecoration) { + SendStateDecoration.SENDING -> { + holder.sendStateImageView + .apply { setImageResource(R.drawable.ic_sending_message) } + .apply { contentDescription = context.getString(R.string.event_status_a11y_sending) } + } + SendStateDecoration.SENT -> { + holder.sendStateImageView + .apply { setImageResource(R.drawable.ic_message_sent) } + .apply { contentDescription = context.getString(R.string.event_status_a11y_sent) } + } + SendStateDecoration.FAILED -> { + holder.sendStateImageView + .apply { setImageResource(R.drawable.ic_sending_message_failed) } + .apply { contentDescription = context.getString(R.string.event_status_a11y_failed) } + } + SendStateDecoration.NONE -> holder.sendStateImageView.isVisible = false + } } override fun unbind(holder: H) { @@ -99,6 +120,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem val avatarImageView by bind(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) + val sendStateImageView by bind(R.id.messageSendStateImageView) } /** diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index 48bd4db94c..25929f7118 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -42,7 +42,8 @@ data class MessageInformationData( val readReceipts: List = emptyList(), val referencesInfoData: ReferencesInfoData? = null, val sentByMe: Boolean, - val e2eDecoration: E2EDecoration = E2EDecoration.NONE + val e2eDecoration: E2EDecoration = E2EDecoration.NONE, + val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE ) : Parcelable { val matrixItem: MatrixItem @@ -84,4 +85,11 @@ enum class E2EDecoration { WARN_SENT_BY_UNKNOWN } +enum class SendStateDecoration { + NONE, + SENDING, + SENT, + FAILED +} + fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) diff --git a/vector/src/main/res/drawable/ic_delete_unsent_messages.xml b/vector/src/main/res/drawable/ic_delete_unsent_messages.xml new file mode 100644 index 0000000000..24fdbc94c2 --- /dev/null +++ b/vector/src/main/res/drawable/ic_delete_unsent_messages.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_message_sent.xml b/vector/src/main/res/drawable/ic_message_sent.xml new file mode 100644 index 0000000000..3729f3d60f --- /dev/null +++ b/vector/src/main/res/drawable/ic_message_sent.xml @@ -0,0 +1,13 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_retry_sending_messages.xml b/vector/src/main/res/drawable/ic_retry_sending_messages.xml new file mode 100644 index 0000000000..6ea08bb654 --- /dev/null +++ b/vector/src/main/res/drawable/ic_retry_sending_messages.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_sending_message.xml b/vector/src/main/res/drawable/ic_sending_message.xml new file mode 100644 index 0000000000..05fa0fb2a2 --- /dev/null +++ b/vector/src/main/res/drawable/ic_sending_message.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_sending_message_failed.xml b/vector/src/main/res/drawable/ic_sending_message_failed.xml new file mode 100644 index 0000000000..c720a1cbbf --- /dev/null +++ b/vector/src/main/res/drawable/ic_sending_message_failed.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index cba12f7515..236222ec1b 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -81,6 +81,7 @@ android:layout_height="wrap_content" android:layout_below="@id/messageMemberNameView" android:layout_toEndOf="@id/messageStartGuideline" + android:layout_toStartOf="@id/messageSendStateImageView" android:addStatesFromChildren="true"> + +