Show send status of text messages.

This commit is contained in:
Onuray Sahin 2021-02-25 13:32:56 +03:00 committed by Benoit Marty
parent 1fe8dfa810
commit c178535cc8
19 changed files with 138 additions and 18 deletions

View File

@ -30,6 +30,7 @@ import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.extensions.nextOrNull 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.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailViewState 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<TimelineEvent>): CacheItemData { private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
val event = items[currentPosition] val event = items[currentPosition]
val nextEvent = items.nextOrNull(currentPosition) val nextEvent = items.nextOrNull(currentPosition)
val prevEvent = items.prevOrNull(currentPosition)
if (hasReachedInvite && hasUTD) { if (hasReachedInvite && hasUTD) {
return CacheItemData(event.localId, event.root.eventId, null, null, null) return CacheItemData(event.localId, event.root.eventId, null, null, null)
} }
updateUTDStates(event, nextEvent) 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.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
} }

View File

@ -52,7 +52,7 @@ class CallItemFactory @Inject constructor(
): VectorEpoxyModel<*>? { ): VectorEpoxyModel<*>? {
if (event.root.eventId == null) return null if (event.root.eventId == null) return null
val roomId = event.roomId val roomId = event.roomId
val informationData = messageInformationDataFactory.create(event, null) val informationData = messageInformationDataFactory.create(event, null, null)
val callSignalingContent = event.getCallSignallingContent() ?: return null val callSignalingContent = event.getCallSignallingContent() ?: return null
val callId = callSignalingContent.callId ?: return null val callId = callSignalingContent.callId ?: return null
val call = callManager.getCallById(callId) val call = callManager.getCallById(callId)

View File

@ -61,7 +61,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
} else { } else {
stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId) 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) return create(text, informationData, highlight, callback)
} }
} }

View File

@ -47,6 +47,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
private val vectorPreferences: VectorPreferences) { private val vectorPreferences: VectorPreferences) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
prevEvent: TimelineEvent?,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { 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<EncryptedEventContent>(), informationData, callback) val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, callback)
return MessageTextItem_() return MessageTextItem_()
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)

View File

@ -48,7 +48,7 @@ class EncryptionItemFactory @Inject constructor(
return null return null
} }
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm
val informationData = informationDataFactory.create(event, null) val informationData = informationDataFactory.create(event, null, null)
val attributes = messageItemAttributesFactory.create(null, informationData, callback) val attributes = messageItemAttributesFactory.create(null, informationData, callback)
val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM

View File

@ -119,13 +119,14 @@ class MessageItemFactory @Inject constructor(
} }
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
prevEvent: TimelineEvent?,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback? callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? { ): VectorEpoxyModel<*>? {
event.root.eventId ?: return null event.root.eventId ?: return null
roomId = event.roomId roomId = event.roomId
val informationData = messageInformationDataFactory.create(event, nextEvent) val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent)
if (event.root.isRedacted()) { if (event.root.isRedacted()) {
// message is redacted // message is redacted
val attributes = messageItemAttributesFactory.create(null, informationData, callback) val attributes = messageItemAttributesFactory.create(null, informationData, callback)

View File

@ -35,7 +35,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?): NoticeItem? { callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null val formattedText = eventFormatter.format(event) ?: return null
val informationData = informationDataFactory.create(event, null) val informationData = informationDataFactory.create(event, null, null)
val attributes = NoticeItem.Attributes( val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer, avatarRenderer = avatarRenderer,
informationData = informationData, informationData = informationData,

View File

@ -38,6 +38,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val userPreferencesProvider: UserPreferencesProvider) { private val userPreferencesProvider: UserPreferencesProvider) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
prevEvent: TimelineEvent?,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
eventIdToHighlight: String?, eventIdToHighlight: String?,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
@ -46,7 +47,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
val computedModel = try { val computedModel = try {
when (event.root.getClearType()) { when (event.root.getClearType()) {
EventType.STICKER, EventType.STICKER,
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, highlight, callback) EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
// State and call // State and call
EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
@ -76,9 +77,9 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.ENCRYPTED -> { EventType.ENCRYPTED -> {
if (event.root.isRedacted()) { if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it // Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(event, nextEvent, highlight, callback) messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
} else { } else {
encryptedItemFactory.create(event, nextEvent, highlight, callback) encryptedItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
} }
} }
EventType.STATE_ROOM_ALIASES, EventType.STATE_ROOM_ALIASES,

View File

@ -75,9 +75,9 @@ class VerificationItemFactory @Inject constructor(
// If it's not a request ignore this event // If it's not a request ignore this event
// if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == null) return ignoredConclusion(event, highlight, callback) // if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == 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) val attributes = messageItemAttributesFactory.create(null, informationData, callback)
when (event.root.getClearType()) { when (event.root.getClearType()) {

View File

@ -64,7 +64,7 @@ class WidgetItemFactory @Inject constructor(
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
widgetContent: WidgetContent, widgetContent: WidgetContent,
previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> { previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
val informationData = informationDataFactory.create(timelineEvent, null) val informationData = informationDataFactory.create(timelineEvent, null, null)
val attributes = messageItemAttributesFactory.create(null, informationData, callback) val attributes = messageItemAttributesFactory.create(null, informationData, callback)
val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName

View File

@ -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.ReactionInfoData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData 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.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.crypto.VerificationState import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.extensions.orFalse 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 dateFormatter: VectorDateFormatter,
private val vectorPreferences: VectorPreferences) { 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 // Non nullability has been tested before
val eventId = event.root.eventId!! 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 time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
val e2eDecoration = getE2EDecoration(event) 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( return MessageInformationData(
eventId = eventId, eventId = eventId,
senderId = event.root.senderId ?: "", senderId = event.root.senderId ?: "",
@ -110,11 +118,24 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
?: VerificationState.REQUEST ?: VerificationState.REQUEST
ReferencesInfoData(verificationState) ReferencesInfoData(verificationState)
}, },
sentByMe = event.root.senderId == session.myUserId, sentByMe = isSentByMe,
e2eDecoration = e2eDecoration 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 { private fun getE2EDecoration(event: TimelineEvent): E2EDecoration {
val roomSummary = roomSummariesHolder.get(event.roomId) val roomSummary = roomSummariesHolder.get(event.roomId)
return if ( return if (

View File

@ -31,7 +31,7 @@ import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController 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) * Adds associated click listeners (on avatar, displayname)
*/ */
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>() { abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>() {
@ -82,6 +82,27 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
holder.avatarImageView.setOnLongClickListener(null) holder.avatarImageView.setOnLongClickListener(null)
holder.memberNameView.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) { override fun unbind(holder: H) {
@ -99,6 +120,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView) val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
val memberNameView by bind<TextView>(R.id.messageMemberNameView) val memberNameView by bind<TextView>(R.id.messageMemberNameView)
val timeView by bind<TextView>(R.id.messageTimeView) val timeView by bind<TextView>(R.id.messageTimeView)
val sendStateImageView by bind<ImageView>(R.id.messageSendStateImageView)
} }
/** /**

View File

@ -42,7 +42,8 @@ data class MessageInformationData(
val readReceipts: List<ReadReceiptData> = emptyList(), val readReceipts: List<ReadReceiptData> = emptyList(),
val referencesInfoData: ReferencesInfoData? = null, val referencesInfoData: ReferencesInfoData? = null,
val sentByMe: Boolean, val sentByMe: Boolean,
val e2eDecoration: E2EDecoration = E2EDecoration.NONE val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE
) : Parcelable { ) : Parcelable {
val matrixItem: MatrixItem val matrixItem: MatrixItem
@ -84,4 +85,11 @@ enum class E2EDecoration {
WARN_SENT_BY_UNKNOWN WARN_SENT_BY_UNKNOWN
} }
enum class SendStateDecoration {
NONE,
SENDING,
SENT,
FAILED
}
fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="30"
android:viewportHeight="30">
<path
android:pathData="M8.5714,22.5C8.5714,23.6864 9.5357,25 10.7143,25H19.2857C20.4643,25 21.4286,23.4428 21.4286,22.2564V11.4711C21.4286,10.2848 20.4643,9.3141 19.2857,9.3141H10.7143C9.5357,9.3141 8.5714,10.2848 8.5714,11.4711V22.5ZM21.4286,6.0785H18.75L17.9893,5.3128C17.7964,5.1186 17.5179,5 17.2393,5H12.7607C12.4821,5 12.2036,5.1186 12.0107,5.3128L11.25,6.0785H8.5714C7.9821,6.0785 7.5,6.5639 7.5,7.1571C7.5,7.7502 7.9821,8.2356 8.5714,8.2356H21.4286C22.0179,8.2356 22.5,7.7502 22.5,7.1571C22.5,6.5639 22.0179,6.0785 21.4286,6.0785Z"
android:fillColor="#FE2928"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M9,16C12.866,16 16,12.866 16,9C16,5.134 12.866,2 9,2C5.134,2 2,5.134 2,9C2,12.866 5.134,16 9,16ZM9,17C13.4183,17 17,13.4183 17,9C17,4.5817 13.4183,1 9,1C4.5817,1 1,4.5817 1,9C1,13.4183 4.5817,17 9,17Z"
android:fillColor="#8D99A5"
android:fillType="evenOdd"/>
<path
android:pathData="M12.8697,5.9531C12.6784,5.7576 12.3597,5.7473 12.1578,5.9325L7.6207,10.048L5.9524,8.9163C5.7293,8.7722 5.4212,8.7722 5.2087,8.9574C4.9536,9.1632 4.9324,9.5336 5.1449,9.7805L7.0681,11.9206C7.1,11.9515 7.1319,11.9926 7.1744,12.0132C7.5356,12.3013 8.0776,12.2498 8.3751,11.9L8.4069,11.8589L12.891,6.6013C13.0397,6.4161 13.0397,6.1383 12.8697,5.9531Z"
android:fillColor="#8D99A5"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M4.0227,2.9646C5.1159,2.1 6.4987,1.5835 8,1.5835C11.3187,1.5835 14.049,4.1029 14.3825,7.3335H15.6723C15.9336,7.3335 16.0894,7.625 15.9445,7.8426L13.9388,10.8543C13.8094,11.0488 13.524,11.0488 13.3945,10.8543L11.3888,7.8426C11.2439,7.625 11.3997,7.3335 11.661,7.3335H12.8719C12.5465,4.9334 10.4893,3.0835 8,3.0835C6.8483,3.0835 5.7909,3.4786 4.9531,4.1411C4.8969,4.1856 4.8485,4.2213 4.813,4.2467C4.7951,4.2595 4.7803,4.2698 4.7692,4.2774L4.7553,4.2869L4.7505,4.2901L4.7487,4.2913L4.7479,4.2918L4.7476,4.2921L4.7474,4.2922L4.7473,4.2922L4.3334,3.6669L4.7472,4.2923C4.4018,4.5209 3.9365,4.4262 3.7079,4.0807C3.4798,3.736 3.5736,3.2719 3.9173,3.0428L3.9202,3.0408L3.9401,3.0268C3.9591,3.0132 3.988,2.992 4.0227,2.9646ZM3.1281,8.6668H4.339C4.6003,8.6668 4.7561,8.3753 4.6112,8.1577L2.6055,5.146C2.476,4.9516 2.1906,4.9516 2.0612,5.146L0.0555,8.1577C-0.0894,8.3753 0.0664,8.6668 0.3277,8.6668H1.6176C1.951,11.8974 4.6813,14.4168 8,14.4168C9.5683,14.4168 11.0069,13.8532 12.1215,12.9184C12.4388,12.6522 12.4803,12.1791 12.2141,11.8617C11.9479,11.5444 11.4749,11.5029 11.1575,11.7691C10.303,12.4859 9.2028,12.9168 8,12.9168C5.5107,12.9168 3.4535,11.0669 3.1281,8.6668Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M9,16C12.866,16 16,12.866 16,9C16,5.134 12.866,2 9,2C5.134,2 2,5.134 2,9C2,12.866 5.134,16 9,16ZM9,17C13.4183,17 17,13.4183 17,9C17,4.5817 13.4183,1 9,1C4.5817,1 1,4.5817 1,9C1,13.4183 4.5817,17 9,17Z"
android:fillColor="#8D99A5"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M8,16C12.4183,16 16,12.4183 16,8C16,3.5817 12.4183,0 8,0C3.5817,0 0,3.5817 0,8C0,12.4183 3.5817,16 8,16ZM6.9806,4.5101C6.9306,3.9401 7.3506,3.4401 7.9206,3.4001C8.4806,3.3601 8.9806,3.7801 9.0406,4.3501V4.5101L8.7206,8.5101C8.6906,8.8801 8.3806,9.1601 8.0106,9.1601H7.9506C7.6006,9.1301 7.3306,8.8601 7.3006,8.5101L6.9806,4.5101ZM8.8801,11.1202C8.8801,11.6062 8.4861,12.0002 8.0001,12.0002C7.5141,12.0002 7.1201,11.6062 7.1201,11.1202C7.1201,10.6342 7.5141,10.2402 8.0001,10.2402C8.4861,10.2402 8.8801,10.6342 8.8801,11.1202Z"
android:fillColor="#FF4B55"
android:fillType="evenOdd"/>
</vector>

View File

@ -81,6 +81,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/messageMemberNameView" android:layout_below="@id/messageMemberNameView"
android:layout_toEndOf="@id/messageStartGuideline" android:layout_toEndOf="@id/messageStartGuideline"
android:layout_toStartOf="@id/messageSendStateImageView"
android:addStatesFromChildren="true"> android:addStatesFromChildren="true">
<ViewStub <ViewStub
@ -133,6 +134,17 @@
</FrameLayout> </FrameLayout>
<ImageView
android:id="@+id/messageSendStateImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_sending_message"
android:layout_alignBottom="@+id/viewStubContainer"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:contentDescription="@string/event_status_a11y_sending" />
<LinearLayout <LinearLayout
android:id="@+id/informationBottom" android:id="@+id/informationBottom"
android:layout_width="match_parent" android:layout_width="match_parent"