Refactors MessageAudioItem to work for both audio files and voice messages

This commit is contained in:
ericdecanini 2022-03-19 18:16:51 +01:00
parent d54b465b30
commit fab78c9a6e
8 changed files with 106 additions and 100 deletions

View File

@ -163,7 +163,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
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.MessageAudioItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
@ -1163,6 +1163,7 @@ class TimelineFragment @Inject constructor(
views.composerLayout.views.sendButton.contentDescription = getString(R.string.action_send)
}
// TODO: Test this
private fun renderSpecialMode(event: TimelineEvent,
@DrawableRes iconRes: Int,
@StringRes descriptionRes: Int,
@ -1175,13 +1176,17 @@ class TimelineFragment @Inject constructor(
}
val messageContent: MessageContent? = event.getLastMessageContent()
val nonFormattedBody = if (messageContent is MessageAudioContent && messageContent.voiceMessageIndicator != null) {
val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
getString(R.string.voice_message_reply_content, formattedDuration)
} else if (messageContent is MessagePollContent) {
messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
} else {
messageContent?.body ?: ""
val nonFormattedBody = when {
messageContent is MessageAudioContent && messageContent.voiceMessageIndicator != null -> {
val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
getString(R.string.voice_message_reply_content, formattedDuration)
}
messageContent is MessagePollContent -> {
messageContent.getBestPollCreationInfo()?.question?.getBestQuestion()
}
else -> {
messageContent?.body ?: ""
}
}
var formattedBody: CharSequence? = null
if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) {
@ -1372,7 +1377,7 @@ class TimelineFragment @Inject constructor(
}
return when (model) {
is MessageFileItem,
is MessageVoiceItem,
is MessageAudioItem,
is MessageImageVideoItem,
is MessageTextItem -> {
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED

View File

@ -43,6 +43,8 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttrib
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.MessageAudioItem
import im.vector.app.features.home.room.detail.timeline.item.MessageAudioItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
@ -52,8 +54,6 @@ 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
@ -96,7 +96,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.PollType
import org.matrix.android.sdk.api.session.room.model.message.getFileName
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
@ -314,36 +313,18 @@ class MessageItemFactory @Inject constructor(
private fun buildAudioMessageItem(params: TimelineItemFactoryParams,
messageContent: MessageAudioContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageVoiceItem? {
val fileUrl = messageContent.getFileUrl()?.let {
if (informationData.sentByMe && !informationData.sendState.isSent()) {
it
} else {
it.takeIf { it.isMxcUrl() }
}
} ?: ""
attributes: AbsMessageItem.Attributes): MessageAudioItem {
val fileUrl = getAudioFileUrl(messageContent, informationData)
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
val playbackControlButtonClickListener: ClickListener = object : ClickListener {
override fun invoke(view: View) {
params.callback?.onVoiceControlButtonClicked(informationData.eventId, messageContent)
}
}
return MessageVoiceItem_()
return MessageAudioItem_()
.attributes(attributes)
.duration(messageContent.audioInfo?.duration ?: 0)
.playbackControlButtonClickListener(playbackControlButtonClickListener)
.voiceMessagePlaybackTracker(voiceMessagePlaybackTracker)
.isLocalFile(localFilesHelper.isLocalFile(fileUrl))
.isDownloaded(session.fileService().isFileInCache(
fileUrl,
messageContent.getFileName(),
messageContent.mimeType,
messageContent.encryptedFileInfo?.toElementToDecrypt())
)
.mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
@ -351,39 +332,42 @@ class MessageItemFactory @Inject constructor(
.leftGuideline(avatarSizeProvider.leftGuideline)
}
private fun getAudioFileUrl(
messageContent: MessageAudioContent,
informationData: MessageInformationData,
) = messageContent.getFileUrl()?.let {
if (informationData.sentByMe && !informationData.sendState.isSent()) {
it
} else {
it.takeIf { it.isMxcUrl() }
}
} ?: ""
private fun createOnPlaybackButtonClickListener(
messageContent: MessageAudioContent,
informationData: MessageInformationData,
params: TimelineItemFactoryParams,
) = object : ClickListener {
override fun invoke(view: View) {
params.callback?.onVoiceControlButtonClicked(informationData.eventId, messageContent)
}
}
private fun buildVoiceMessageItem(params: TimelineItemFactoryParams,
messageContent: MessageAudioContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
attributes: AbsMessageItem.Attributes): MessageVoiceItem? {
val fileUrl = messageContent.getFileUrl()?.let {
if (informationData.sentByMe && !informationData.sendState.isSent()) {
it
} else {
it.takeIf { it.isMxcUrl() }
}
} ?: ""
attributes: AbsMessageItem.Attributes): MessageAudioItem {
val fileUrl = getAudioFileUrl(messageContent, informationData)
val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params)
val playbackControlButtonClickListener: ClickListener = object : ClickListener {
override fun invoke(view: View) {
params.callback?.onVoiceControlButtonClicked(informationData.eventId, messageContent)
}
}
return MessageVoiceItem_()
return MessageAudioItem_()
.attributes(attributes)
.duration(messageContent.audioWaveformInfo?.duration ?: 0)
.waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty())
.playbackControlButtonClickListener(playbackControlButtonClickListener)
.voiceMessagePlaybackTracker(voiceMessagePlaybackTracker)
.isLocalFile(localFilesHelper.isLocalFile(fileUrl))
.isDownloaded(session.fileService().isFileInCache(
fileUrl,
messageContent.getFileName(),
messageContent.mimeType,
messageContent.encryptedFileInfo?.toElementToDecrypt())
)
.mxcUrl(fileUrl)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
@ -432,10 +416,8 @@ class MessageItemFactory @Inject constructor(
}
private fun buildFileMessageItem(messageContent: MessageFileContent,
// informationData: MessageInformationData,
highlight: Boolean,
// callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): MessageFileItem? {
attributes: AbsMessageItem.Attributes): MessageFileItem {
val mxcUrl = messageContent.getFileUrl() ?: ""
return MessageFileItem_()
.attributes(attributes)

View File

@ -16,6 +16,7 @@
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
@ -36,7 +37,7 @@ import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLay
import im.vector.app.features.themes.ThemeUtils
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
@EpoxyAttribute
var mxcUrl: String = ""
@ -53,7 +54,7 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
@EpoxyAttribute
@JvmField
var isDownloaded = false
var isVoiceMessage = false
@EpoxyAttribute
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
@ -69,21 +70,21 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
override fun bind(holder: Holder) {
super.bind(holder)
renderSendState(holder.voiceLayout, null)
renderSendState(holder.audioLayout, 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.audioPlaybackControlButton.setImageResource(R.drawable.ic_cross)
holder.audioPlaybackControlButton.contentDescription = getUnableToPlayContentDescription(holder.view.context)
holder.progressLayout.isVisible = false
}
holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener)
holder.audioPlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener)
holder.voicePlaybackWaveform.post {
holder.voicePlaybackWaveform.recreate()
holder.audioPlaybackWaveform.post {
holder.audioPlaybackWaveform.recreate()
waveform.forEach { amplitude ->
holder.voicePlaybackWaveform.update(amplitude)
holder.audioPlaybackWaveform.update(amplitude)
}
}
@ -92,8 +93,8 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
else
ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary)
holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
holder.audioPlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
override fun onUpdate(state: VoiceMessagePlaybackTracker.Listener.State) {
@ -107,22 +108,34 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.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.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)
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
holder.audioPlaybackControlButton.contentDescription = getPlayMessageContentDescription(holder.view.context)
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: VoiceMessagePlaybackTracker.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)
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause)
holder.audioPlaybackControlButton.contentDescription = getPauseMessageContentDescription(holder.view.context)
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: VoiceMessagePlaybackTracker.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)
holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play)
holder.audioPlaybackControlButton.contentDescription = getPlayMessageContentDescription(holder.view.context)
holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime)
}
private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong())
@ -137,15 +150,15 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
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)
val audioPlaybackLayout by bind<View>(R.id.audioPlaybackLayout)
val audioLayout by bind<ViewGroup>(R.id.audioLayout)
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)
}
companion object {
private const val STUB_ID = R.id.messageContentVoiceStub
private const val STUB_ID = R.id.messageContentAudioStub
}
}

View File

@ -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.messageFileUploadProgressLayout)
val progressLayout by bind<ViewGroup>(R.id.audioFileUploadProgressLayout)
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

@ -2,7 +2,7 @@
<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:id="@+id/audioLayout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -10,13 +10,13 @@
tools:viewBindingIgnore="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/voicePlaybackLayout"
android:id="@+id/audioPlaybackLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TimelineContentMediaPillStyle">
<ImageButton
android:id="@+id/voicePlaybackControlButton"
android:id="@+id/audioPlaybackControlButton"
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"
@ -29,19 +29,19 @@
app:tint="?vctr_content_secondary" />
<TextView
android:id="@+id/voicePlaybackTime"
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/voicePlaybackControlButton"
app:layout_constraintStart_toEndOf="@id/voicePlaybackControlButton"
app:layout_constraintTop_toTopOf="@id/voicePlaybackControlButton"
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/voicePlaybackWaveform"
android:id="@+id/audioPlaybackWaveform"
style="@style/VoicePlaybackWaveform"
android:layout_width="0dp"
android:layout_height="0dp"
@ -50,13 +50,13 @@
android:importantForAccessibility="no"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/voicePlaybackTime"
app:layout_constraintStart_toEndOf="@id/audioPlaybackTime"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/messageFileUploadProgressLayout"
android:id="@+id/audioFileUploadProgressLayout"
layout="@layout/media_upload_download_progress_layout"
android:layout_width="match_parent"
android:layout_height="46dp"

View File

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

View File

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

View File

@ -2858,6 +2858,12 @@
<string name="error_voice_message_cannot_reply_or_edit">Cannot reply or edit while voice message is active</string>
<string name="voice_message_reply_content">Voice Message (%1$s)</string>
<string name="a11y_play_audio_message">Play Audio Message</string>
<string name="a11y_pause_audio_message">Pause Audio Message</string>
<string name="error_audio_message_unable_to_play">Pause Audio Message</string>
<string name="error_audio_message_cannot_reply_or_edit">Cannot reply or edit while audio message is active</string>
<string name="audio_message_reply_content">Audio Message (%1$s)</string>
<string name="upgrade_room_for_restricted">Anyone in %s will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string>
<string name="upgrade_room_for_restricted_no_param">Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. Youll be able to change this in room settings anytime.</string>