Update voice message recording button behaviour (#1784)

Changes recording button behaviour so that
- tapping the record button starts a recording and displays the stop button
- tapping the stop button stops the recording
- tapping the delete button cancels the recording
- 'hold to record' tooltip is removed


---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
jonnyandrew
2023-11-14 14:05:59 +00:00
committed by GitHub
parent 1df3f6113a
commit 0b1d41e861
37 changed files with 221 additions and 576 deletions
@@ -32,7 +32,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.textcomposer.TextComposer
import io.element.android.libraries.textcomposer.model.Message
import io.element.android.libraries.textcomposer.model.PressEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
import io.element.android.libraries.textcomposer.model.Suggestion
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
import kotlinx.coroutines.launch
@@ -77,8 +77,8 @@ internal fun MessageComposerView(
}
}
val onVoiceRecordButtonEvent = { press: PressEvent ->
voiceMessageState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(press))
val onVoiceRecorderEvent = { press: VoiceMessageRecorderEvent ->
voiceMessageState.eventSink(VoiceMessageComposerEvents.RecorderEvent(press))
}
val onSendVoiceMessage = {
@@ -107,7 +107,7 @@ internal fun MessageComposerView(
onDismissTextFormatting = ::onDismissTextFormatting,
enableTextFormatting = enableTextFormatting,
enableVoiceMessages = enableVoiceMessages,
onVoiceRecordButtonEvent = onVoiceRecordButtonEvent,
onVoiceRecorderEvent = onVoiceRecorderEvent,
onVoicePlayerEvent = onVoicePlayerEvent,
onSendVoiceMessage = onSendVoiceMessage,
onDeleteVoiceMessage = onDeleteVoiceMessage,
@@ -17,12 +17,12 @@
package io.element.android.features.messages.impl.voicemessages.composer
import androidx.lifecycle.Lifecycle
import io.element.android.libraries.textcomposer.model.PressEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
sealed interface VoiceMessageComposerEvents {
data class RecordButtonEvent(
val pressEvent: PressEvent
data class RecorderEvent(
val recorderEvent: VoiceMessageRecorderEvent
): VoiceMessageComposerEvents
data class PlayerEvent(
val playerEvent: VoiceMessagePlayerEvent,
@@ -37,7 +37,7 @@ import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.permissions.api.PermissionsEvents
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.textcomposer.model.PressEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageState
import io.element.android.libraries.voicerecorder.api.VoiceRecorder
@@ -95,10 +95,10 @@ class VoiceMessageComposerPresenter @Inject constructor(
}
}
val onRecordButtonPress = { event: VoiceMessageComposerEvents.RecordButtonEvent ->
val onVoiceMessageRecorderEvent = { event: VoiceMessageComposerEvents.RecorderEvent ->
val permissionGranted = permissionState.permissionGranted
when (event.pressEvent) {
PressEvent.PressStart -> {
when (event.recorderEvent) {
VoiceMessageRecorderEvent.Start -> {
Timber.v("Voice message record button pressed")
when {
permissionGranted -> {
@@ -110,12 +110,12 @@ class VoiceMessageComposerPresenter @Inject constructor(
}
}
}
PressEvent.LongPressEnd -> {
Timber.v("Voice message record button released")
VoiceMessageRecorderEvent.Stop -> {
Timber.v("Voice message stop button pressed")
localCoroutineScope.finishRecording()
}
PressEvent.Tapped -> {
Timber.v("Voice message record button tapped")
VoiceMessageRecorderEvent.Cancel -> {
Timber.v("Voice message cancel button tapped")
localCoroutineScope.cancelRecording()
}
}
@@ -163,7 +163,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
val handleEvents: (VoiceMessageComposerEvents) -> Unit = { event ->
when (event) {
is VoiceMessageComposerEvents.RecordButtonEvent -> onRecordButtonPress(event)
is VoiceMessageComposerEvents.RecorderEvent -> onVoiceMessageRecorderEvent(event)
is VoiceMessageComposerEvents.PlayerEvent -> onPlayerEvent(event.playerEvent)
is VoiceMessageComposerEvents.SendVoiceMessage -> localCoroutineScope.launch {
onSendButtonPress()
@@ -44,7 +44,7 @@ import io.element.android.libraries.permissions.api.aPermissionsState
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.PressEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageState
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
@@ -99,7 +99,7 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
@@ -116,13 +116,13 @@ class VoiceMessageComposerPresenterTest {
presenter.present()
}.test {
awaitItem().apply {
eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
assertThat(keepScreenOn).isFalse()
}
awaitItem().apply {
assertThat(keepScreenOn).isTrue()
eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
}
val finalState = awaitItem().apply {
@@ -139,13 +139,11 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.Tapped))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Cancel))
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1)
testPauseAndDestroy(finalState)
}
}
@@ -156,8 +154,8 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState())
@@ -173,7 +171,7 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
val finalState = awaitItem().apply {
this.eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
}
@@ -192,8 +190,8 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
val finalState = awaitItem().also {
assertThat(it.voiceMessageState).isEqualTo(aPlayingState())
@@ -210,8 +208,8 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Pause))
val finalState = awaitItem().also {
@@ -229,8 +227,8 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Seek(0.5f)))
awaitItem().apply {
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.5f, time = 0.seconds, showCursor = true))
@@ -256,8 +254,8 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage)
val finalState = awaitItem()
@@ -274,8 +272,8 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage)
awaitItem().apply {
@@ -296,8 +294,8 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState().toSendingState())
@@ -318,15 +316,15 @@ class VoiceMessageComposerPresenterTest {
}.test {
// Send a normal voice message
messageComposerContext.composerMode = MessageComposerMode.Normal
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
skipItems(1) // Sending state
// Now reply with a voice message
messageComposerContext.composerMode = aReplyMode()
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
val finalState = awaitItem() // Sending state
@@ -345,8 +343,8 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
assertThat(awaitItem().voiceMessageState).isEqualTo(aPlayingState().toSendingState())
@@ -367,8 +365,8 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem().run {
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
@@ -392,8 +390,8 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
awaitItem().apply {
assertThat(voiceMessageState).isEqualTo(aPreviewState())
eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
@@ -417,8 +415,8 @@ class VoiceMessageComposerPresenterTest {
presenter.present()
}.test {
mediaPreProcessor.givenResult(Result.failure(Exception()))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
val previewState = awaitItem()
previewState.eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
@@ -467,7 +465,7 @@ class VoiceMessageComposerPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
assertThat(matrixRoom.sendMediaCount).isEqualTo(0)
assertThat(analyticsService.trackedErrors).containsExactly(
@@ -491,15 +489,15 @@ class VoiceMessageComposerPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle)
initialState.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
initialState.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Stop))
voiceRecorder.assertCalls(stopped = 1)
permissionsPresenter.setPermissionGranted()
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
voiceRecorder.assertCalls(stopped = 1, started = 1)
@@ -519,7 +517,7 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
// See the dialog and accept it
awaitItem().also {
@@ -533,7 +531,7 @@ class VoiceMessageComposerPresenterTest {
permissionsPresenter.setPermissionGranted()
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
voiceRecorder.assertCalls(started = 1)
@@ -553,7 +551,7 @@ class VoiceMessageComposerPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
awaitItem().eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
// See the dialog and accept it
awaitItem().also {
@@ -565,7 +563,7 @@ class VoiceMessageComposerPresenterTest {
// Dialog is hidden, user tries to record again
awaitItem().also {
assertThat(it.showPermissionRationaleDialog).isFalse()
it.eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.PressStart))
it.eventSink(VoiceMessageComposerEvents.RecorderEvent(VoiceMessageRecorderEvent.Start))
}
// Dialog is shown once again