Adds new audio timeline stub

This commit is contained in:
ericdecanini 2022-03-21 18:42:07 +01:00
parent 7a7d36d010
commit ff26829d65
8 changed files with 292 additions and 79 deletions

View File

@ -54,6 +54,8 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem
import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_
import im.vector.app.features.home.room.detail.timeline.item.PollItem
import im.vector.app.features.home.room.detail.timeline.item.PollItem_
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
@ -321,6 +323,7 @@ class MessageItemFactory @Inject constructor(
return MessageAudioItem_()
.attributes(attributes)
.filename(messageContent.body)
.duration(messageContent.audioInfo?.duration ?: 0)
.playbackControlButtonClickListener(playbackControlButtonClickListener)
.audioMessagePlaybackTracker(audioMessagePlaybackTracker)
@ -357,16 +360,16 @@ class MessageItemFactory @Inject constructor(
messageContent: MessageAudioContent,
informationData: MessageInformationData,
highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageAudioItem {
attributes: AbsMessageItem.Attributes): MessageVoiceItem {
val fileUrl = getAudioFileUrl(messageContent, informationData)
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
return MessageAudioItem_()
return MessageVoiceItem_()
.attributes(attributes)
.duration(messageContent.audioWaveformInfo?.duration ?: 0)
.waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty())
.playbackControlButtonClickListener(playbackControlButtonClickListener)
.audioMessagePlaybackTracker(audioMessagePlaybackTracker)
.voiceMessagePlaybackTracker(audioMessagePlaybackTracker)
.isLocalFile(localFilesHelper.isLocalFile(fileUrl))
.mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)

View File

@ -16,18 +16,15 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.text.format.DateUtils
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.visualizer.amplitude.AudioRecordView
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
@ -39,23 +36,19 @@ import im.vector.app.features.themes.ThemeUtils
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
@EpoxyAttribute
var filename: String = ""
@EpoxyAttribute
var mxcUrl: String = ""
@EpoxyAttribute
var duration: Int = 0
@EpoxyAttribute
var waveform: List<Int> = emptyList()
@EpoxyAttribute
@JvmField
var isLocalFile = false
@EpoxyAttribute
@JvmField
var isVoiceMessage = false
@EpoxyAttribute
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
@ -70,32 +63,34 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
override fun bind(holder: Holder) {
super.bind(holder)
renderSendState(holder.audioLayout, null)
renderSendState(holder.rootLayout, null)
bindUploadState(holder)
holder.filenameView.text = filename
applyLayoutTint(holder)
holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
renderStateBasedOnAudioPlayback(holder)
}
private fun bindUploadState(holder: Holder) {
if (!attributes.informationData.sendState.hasFailed()) {
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, isLocalFile, holder.progressLayout)
} else {
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_cross)
holder.audioPlaybackControlButton.contentDescription = getUnableToPlayContentDescription(holder.view.context)
holder.audioPlaybackControlButton.contentDescription = holder.view.context.getString(R.string.error_audio_message_unable_to_play)
holder.progressLayout.isVisible = false
}
}
holder.audioPlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener)
holder.audioPlaybackWaveform.post {
holder.audioPlaybackWaveform.recreate()
waveform.forEach { amplitude ->
holder.audioPlaybackWaveform.update(amplitude)
}
}
private fun applyLayoutTint(holder: Holder) {
val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble)
Color.TRANSPARENT
else
ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary)
holder.audioPlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
}
private fun renderStateBasedOnAudioPlayback(holder: Holder) {
audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
when (state) {
@ -108,33 +103,21 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
})
}
private fun getUnableToPlayContentDescription(context: Context) = context.getString(
if (isVoiceMessage) R.string.error_voice_message_unable_to_play else R.string.error_audio_message_unable_to_play
)
private fun renderIdleState(holder: Holder) {
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
holder.audioPlaybackControlButton.contentDescription = getPlayMessageContentDescription(holder.view.context)
holder.audioPlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_audio_message)
holder.audioPlaybackTime.text = formatPlaybackTime(duration)
}
private fun getPlayMessageContentDescription(context: Context) = context.getString(
if (isVoiceMessage) R.string.a11y_play_voice_message else R.string.a11y_play_audio_message
)
private fun renderPlayingState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Playing) {
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
holder.audioPlaybackControlButton.contentDescription = getPauseMessageContentDescription(holder.view.context)
holder.audioPlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_pause_audio_message)
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
}
private fun getPauseMessageContentDescription(context: Context) = context.getString(
if (isVoiceMessage) R.string.a11y_pause_voice_message else R.string.a11y_pause_audio_message
)
private fun renderPausedState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Paused) {
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
holder.audioPlaybackControlButton.contentDescription = getPlayMessageContentDescription(holder.view.context)
holder.audioPlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_audio_message)
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
}
@ -150,12 +133,12 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val audioPlaybackLayout by bind<View>(R.id.audioPlaybackLayout)
val audioLayout by bind<ViewGroup>(R.id.audioLayout)
val rootLayout by bind<ViewGroup>(R.id.messageRootLayout)
val mainLayout by bind<ViewGroup>(R.id.messageMainInnerLayout)
val filenameView by bind<TextView>(R.id.messageFilenameView)
val audioPlaybackControlButton by bind<ImageButton>(R.id.audioPlaybackControlButton)
val audioPlaybackTime by bind<TextView>(R.id.audioPlaybackTime)
val audioPlaybackWaveform by bind<AudioRecordView>(R.id.audioPlaybackWaveform)
val progressLayout by bind<ViewGroup>(R.id.audioFileUploadProgressLayout)
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
}
companion object {

View File

@ -47,9 +47,6 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
@DrawableRes
var iconRes: Int = 0
// @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
// var clickListener: ClickListener? = null
@EpoxyAttribute
@JvmField
var isLocalFile = false
@ -67,13 +64,16 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
override fun bind(holder: Holder) {
super.bind(holder)
renderSendState(holder.fileLayout, holder.filenameView)
if (!attributes.informationData.sendState.hasFailed()) {
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, isLocalFile, holder.progressLayout)
} else {
holder.fileImageView.setImageResource(R.drawable.ic_cross)
holder.progressLayout.isVisible = false
}
holder.filenameView.text = filename
if (attributes.informationData.sendState.isSending()) {
holder.fileImageView.setImageResource(iconRes)
} else {
@ -85,7 +85,7 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
holder.fileImageView.setImageResource(R.drawable.ic_download)
}
}
// holder.view.setOnClickListener(clickListener)
val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
Color.TRANSPARENT
} else {
@ -109,7 +109,7 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
class Holder : AbsMessageItem.Holder(STUB_ID) {
val mainLayout by bind<ViewGroup>(R.id.messageFileMainLayout)
val progressLayout by bind<ViewGroup>(R.id.audioFileUploadProgressLayout)
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
val fileImageView by bind<ImageView>(R.id.messageFileIconView)
val fileImageWrapper by bind<ViewGroup>(R.id.messageFileImageView)

View File

@ -0,0 +1,150 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.item
import android.content.res.ColorStateList
import android.graphics.Color
import android.text.format.DateUtils
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.visualizer.amplitude.AudioRecordView
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.themes.ThemeUtils
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
@EpoxyAttribute
var mxcUrl: String = ""
@EpoxyAttribute
var duration: Int = 0
@EpoxyAttribute
var waveform: List<Int> = emptyList()
@EpoxyAttribute
@JvmField
var isLocalFile = false
@EpoxyAttribute
@JvmField
var isDownloaded = false
@EpoxyAttribute
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
@EpoxyAttribute
lateinit var contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var playbackControlButtonClickListener: ClickListener? = null
@EpoxyAttribute
lateinit var voiceMessagePlaybackTracker: AudioMessagePlaybackTracker
override fun bind(holder: Holder) {
super.bind(holder)
renderSendState(holder.voiceLayout, null)
if (!attributes.informationData.sendState.hasFailed()) {
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, isLocalFile, holder.progressLayout)
} else {
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_cross)
holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.error_voice_message_unable_to_play)
holder.progressLayout.isVisible = false
}
holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener)
holder.voicePlaybackWaveform.post {
holder.voicePlaybackWaveform.recreate()
waveform.forEach { amplitude ->
holder.voicePlaybackWaveform.update(amplitude)
}
}
val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
Color.TRANSPARENT
} else {
ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary)
}
holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener {
override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) {
when (state) {
is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder)
is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state)
is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state)
}
}
})
}
private fun renderIdleState(holder: Holder) {
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message)
holder.voicePlaybackTime.text = formatPlaybackTime(duration)
}
private fun renderPlayingState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Playing) {
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_pause_voice_message)
holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime)
}
private fun renderPausedState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Paused) {
holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message)
holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime)
}
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
override fun unbind(holder: Holder) {
super.unbind(holder)
contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId)
contentDownloadStateTrackerBinder.unbind(mxcUrl)
voiceMessagePlaybackTracker.untrack(attributes.informationData.eventId)
}
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val voicePlaybackLayout by bind<View>(R.id.voicePlaybackLayout)
val voiceLayout by bind<ViewGroup>(R.id.voiceLayout)
val voicePlaybackControlButton by bind<ImageButton>(R.id.voicePlaybackControlButton)
val voicePlaybackTime by bind<TextView>(R.id.voicePlaybackTime)
val voicePlaybackWaveform by bind<AudioRecordView>(R.id.voicePlaybackWaveform)
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
}
companion object {
private const val STUB_ID = R.id.messageContentVoiceStub
}
}

View File

@ -2,18 +2,18 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/audioLayout"
android:orientation="vertical"
android:id="@+id/messageRootLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="@dimen/chat_bubble_fixed_size"
android:orientation="vertical"
tools:viewBindingIgnore="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/audioPlaybackLayout"
android:layout_width="match_parent"
android:id="@+id/messageMainInnerLayout"
style="@style/TimelineContentMediaPillStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TimelineContentMediaPillStyle">
tools:viewBindingIgnore="true">
<ImageButton
android:id="@+id/audioPlaybackControlButton"
@ -23,44 +23,45 @@
android:backgroundTint="?android:colorBackground"
android:contentDescription="@string/a11y_play_voice_message"
android:src="@drawable/ic_play_pause_play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:tint="?vctr_content_secondary" />
<TextView
android:id="@+id/messageFilenameView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:autoLink="none"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/audioPlaybackControlButton"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/audioPlaybackTime"
tools:text="A filename here" />
<TextView
android:id="@+id/audioPlaybackTime"
style="@style/Widget.Vector.TextView.Body.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toBottomOf="@id/audioPlaybackControlButton"
app:layout_constraintStart_toEndOf="@id/audioPlaybackControlButton"
app:layout_constraintTop_toTopOf="@id/audioPlaybackControlButton"
tools:text="0:23" />
<com.visualizer.amplitude.AudioRecordView
android:id="@+id/audioPlaybackWaveform"
style="@style/VoicePlaybackWaveform"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:importantForAccessibility="no"
app:layout_constraintStart_toStartOf="@id/messageFilenameView"
app:layout_constraintTop_toBottomOf="@id/messageFilenameView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/audioPlaybackTime"
app:layout_constraintTop_toTopOf="parent" />
tools:text="0:23" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/audioFileUploadProgressLayout"
android:id="@+id/messageFileUploadProgressLayout"
layout="@layout/media_upload_download_progress_layout"
android:layout_width="match_parent"
android:layout_height="46dp"
android:layout_marginStart="8dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:visibility="gone"

View File

@ -49,7 +49,7 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/audioFileUploadProgressLayout"
android:id="@+id/messageFileUploadProgressLayout"
layout="@layout/media_upload_download_progress_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -33,6 +33,13 @@
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_redacted_stub" />
<ViewStub
android:id="@+id/messageContentVoiceStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_voice_stub"
tools:visibility="gone" />
<ViewStub
android:id="@+id/messageContentAudioStub"
android:layout_width="match_parent"

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/voiceLayout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="@dimen/chat_bubble_fixed_size"
tools:viewBindingIgnore="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/voicePlaybackLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TimelineContentMediaPillStyle">
<ImageButton
android:id="@+id/voicePlaybackControlButton"
android:layout_width="@dimen/item_event_message_media_button_size"
android:layout_height="@dimen/item_event_message_media_button_size"
android:background="@drawable/bg_voice_play_pause_button"
android:backgroundTint="?android:colorBackground"
android:contentDescription="@string/a11y_play_voice_message"
android:src="@drawable/ic_play_pause_play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?vctr_content_secondary" />
<TextView
android:id="@+id/voicePlaybackTime"
style="@style/Widget.Vector.TextView.Body.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toBottomOf="@id/voicePlaybackControlButton"
app:layout_constraintStart_toEndOf="@id/voicePlaybackControlButton"
app:layout_constraintTop_toTopOf="@id/voicePlaybackControlButton"
tools:text="0:23" />
<com.visualizer.amplitude.AudioRecordView
android:id="@+id/voicePlaybackWaveform"
style="@style/VoicePlaybackWaveform"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/voicePlaybackTime"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/messageFileUploadProgressLayout"
layout="@layout/media_upload_download_progress_layout"
android:layout_width="match_parent"
android:layout_height="46dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>