diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 2da69bbe6c..d019cb1777 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -2051,6 +2051,14 @@ class TimelineFragment @Inject constructor( messageComposerViewModel.handle(MessageComposerAction.PlayOrPauseVoicePlayback(eventId, messageAudioContent)) } + override fun onVoiceWaveformTouchedUp(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float) { + messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformTouchedUp(eventId, messageAudioContent, percentage)) + } + + override fun onVoiceWaveformMovedTo(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float) { + messageComposerViewModel.handle(MessageComposerAction.VoiceWaveformMovedTo(eventId, messageAudioContent, percentage)) + } + private fun onShareActionClicked(action: EventSharedAction.Share) { when (action.messageContent) { is MessageTextContent -> shareText(requireContext(), action.messageContent.body) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt index 10cef39942..daa5631d84 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerAction.kt @@ -40,4 +40,6 @@ sealed class MessageComposerAction : VectorViewModelAction { data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : MessageComposerAction() object PlayOrPauseRecordingPlayback : MessageComposerAction() data class EndAllVoiceActions(val deleteRecord: Boolean = true) : MessageComposerAction() + data class VoiceWaveformTouchedUp(val eventId: String, val messageAudioContent: MessageAudioContent, val percentage: Float) : MessageComposerAction() + data class VoiceWaveformMovedTo(val eventId: String, val messageAudioContent: MessageAudioContent, val percentage: Float) : MessageComposerAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index 0d90227168..ccb51d3796 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -108,7 +108,9 @@ class MessageComposerViewModel @AssistedInject constructor( is MessageComposerAction.EndAllVoiceActions -> handleEndAllVoiceActions(action.deleteRecord) is MessageComposerAction.InitializeVoiceRecorder -> handleInitializeVoiceRecorder(action.attachmentData) is MessageComposerAction.OnEntersBackground -> handleEntersBackground(action.composerText) - } + is MessageComposerAction.VoiceWaveformTouchedUp -> handleVoiceWaveformTouchedUp(action) + is MessageComposerAction.VoiceWaveformMovedTo -> handleVoiceWaveformMovedTo(action) + }.exhaustive } private fun handleOnVoiceRecordingUiStateChanged(action: MessageComposerAction.OnVoiceRecordingUiStateChanged) = setState { @@ -861,6 +863,18 @@ class MessageComposerViewModel @AssistedInject constructor( voiceMessageHelper.pauseRecording() } + private fun handleVoiceWaveformTouchedUp(action: MessageComposerAction.VoiceWaveformTouchedUp) { + val duration = (action.messageAudioContent.audioInfo?.duration ?: 0) + val toMillisecond = (action.percentage * duration).toInt() + voiceMessageHelper.movePlaybackTo(action.eventId, toMillisecond, duration) + } + + private fun handleVoiceWaveformMovedTo(action: MessageComposerAction.VoiceWaveformMovedTo) { + val duration = (action.messageAudioContent.audioInfo?.duration ?: 0) + val toMillisecond = (action.percentage * duration).toInt() + voiceMessageHelper.movePlaybackTo(action.eventId, toMillisecond, duration) + } + private fun handleEntersBackground(composerText: String) { val isVoiceRecording = com.airbnb.mvrx.withState(this) { it.isVoiceRecording } if (isVoiceRecording) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt index f9dfecd1f5..b6a8dc2cd5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageHelper.kt @@ -174,6 +174,14 @@ class VoiceMessageHelper @Inject constructor( stopPlaybackTicker() } + fun movePlaybackTo(id: String, toMillisecond: Int, totalDuration: Int) { + val percentage = toMillisecond.toFloat() / totalDuration + playbackTracker.updateCurrentPlaybackTime(id, toMillisecond, percentage) + + stopPlayback() + playbackTracker.pausePlayback(id) + } + private fun startRecordingAmplitudes() { amplitudeTicker?.stop() amplitudeTicker = CountUpTimer(50).apply { 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 2ac592797c..3965afdbaa 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 @@ -138,6 +138,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec fun getPreviewUrlRetriever(): PreviewUrlRetriever fun onVoiceControlButtonClicked(eventId: String, messageAudioContent: MessageAudioContent) + fun onVoiceWaveformTouchedUp(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float) + fun onVoiceWaveformMovedTo(eventId: String, messageAudioContent: MessageAudioContent, percentage: Float) } interface ReactionPillCallback { 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 da97cf6984..8b0b43009d 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 @@ -357,11 +357,22 @@ class MessageItemFactory @Inject constructor( } } + val waveformTouchListener: MessageVoiceItem.WaveformTouchListener = object : MessageVoiceItem.WaveformTouchListener { + override fun onWaveformTouchedUp(percentage: Float) { + params.callback?.onVoiceWaveformTouchedUp(informationData.eventId, messageContent, percentage) + } + + override fun onWaveformMovedTo(percentage: Float) { + params.callback?.onVoiceWaveformMovedTo(informationData.eventId, messageContent, percentage) + } + } + return MessageVoiceItem_() .attributes(attributes) .duration(messageContent.audioWaveformInfo?.duration ?: 0) .waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty()) .playbackControlButtonClickListener(playbackControlButtonClickListener) + .waveformTouchListener(waveformTouchListener) .voiceMessagePlaybackTracker(voiceMessagePlaybackTracker) .izLocalFile(localFilesHelper.isLocalFile(fileUrl)) .izDownloaded(session.fileService().isFileInCache( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt index 82400a431d..d1c134a743 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -19,6 +19,7 @@ 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.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.ImageButton @@ -38,6 +39,11 @@ import im.vector.app.features.voice.AudioWaveformView @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageVoiceItem : AbsMessageItem() { + interface WaveformTouchListener { + fun onWaveformTouchedUp(percentage: Float) + fun onWaveformMovedTo(percentage: Float) + } + @EpoxyAttribute var mxcUrl: String = "" @@ -62,6 +68,9 @@ abstract class MessageVoiceItem : AbsMessageItem() { @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var playbackControlButtonClickListener: ClickListener? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var waveformTouchListener: WaveformTouchListener? = null + @EpoxyAttribute lateinit var voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker @@ -87,6 +96,20 @@ abstract class MessageVoiceItem : AbsMessageItem() { holder.voicePlaybackWaveform.add(AudioWaveformView.FFT(amplitude.toFloat(), waveformColorIdle)) } holder.voicePlaybackWaveform.summarize() + + holder.voicePlaybackWaveform.setOnTouchListener { view, motionEvent -> + when (motionEvent.action) { + MotionEvent.ACTION_UP -> { + val percentage = getTouchedPositionPercentage(motionEvent, view) + waveformTouchListener?.onWaveformTouchedUp(percentage) + } + MotionEvent.ACTION_MOVE -> { + val percentage = getTouchedPositionPercentage(motionEvent, view) + waveformTouchListener?.onWaveformMovedTo(percentage) + } + } + true + } } val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) { @@ -111,6 +134,8 @@ abstract class MessageVoiceItem : AbsMessageItem() { } } + private fun getTouchedPositionPercentage(motionEvent: MotionEvent, view: View) = motionEvent.x / view.width + private fun renderIdleState(holder: Holder, idleColor: Int, playedColor: Int) { holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message)