From 0b1d41e861f872ff3928d6e91e0dbec9046386ab Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Tue, 14 Nov 2023 14:05:59 +0000 Subject: [PATCH] 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 --- changelog.d/1784.feature | 1 + .../messagecomposer/MessageComposerView.kt | 8 +- .../composer/VoiceMessageComposerEvents.kt | 6 +- .../composer/VoiceMessageComposerPresenter.kt | 18 +- .../VoiceMessageComposerPresenterTest.kt | 84 ++++---- .../src/main/res/drawable/ic_pause.xml | 0 .../src/main/res/drawable/ic_play.xml | 0 .../src/main/res/drawable/ic_stop.xml | 9 + .../libraries/textcomposer/TextComposer.kt | 25 ++- .../textcomposer/components/RecordButton.kt | 189 ------------------ .../components/VoiceMessagePreview.kt | 6 +- .../components/VoiceMessageRecorderButton.kt | 119 +++++++++++ ...sEvent.kt => VoiceMessageRecorderEvent.kt} | 8 +- .../textcomposer/utils/PressState.kt | 31 --- .../textcomposer/utils/PressStateEffects.kt | 47 ----- .../textcomposer/utils/PressStateHolder.kt | 101 ---------- .../utils/PressStateHolderTest.kt | 111 ---------- ...ewVoice-Day-6_6_null_0,NEXUS_5,1.0,en].png | 4 +- ...Voice-Night-6_7_null_0,NEXUS_5,1.0,en].png | 4 +- ...Tooltip-Day-13_13_null,NEXUS_5,1.0,en].png | 3 - ...oltip-Night-13_14_null,NEXUS_5,1.0,en].png | 3 - ...dButton-Day-12_12_null,NEXUS_5,1.0,en].png | 3 - ...utton-Night-12_13_null,NEXUS_5,1.0,en].png | 3 - ...Button-Day-12_12_null,NEXUS_5,1.0,en].png} | 0 ...tton-Night-12_13_null,NEXUS_5,1.0,en].png} | 0 ...atting-Day-13_13_null,NEXUS_5,1.0,en].png} | 0 ...ting-Night-13_14_null,NEXUS_5,1.0,en].png} | 0 ...Button-Day-14_14_null,NEXUS_5,1.0,en].png} | 0 ...tton-Night-14_15_null,NEXUS_5,1.0,en].png} | 0 ...review-Day-15_15_null,NEXUS_5,1.0,en].png} | 0 ...view-Night-15_16_null,NEXUS_5,1.0,en].png} | 0 ...rButton-Day-16_16_null,NEXUS_5,1.0,en].png | 3 + ...utton-Night-16_17_null,NEXUS_5,1.0,en].png | 3 + ...ording-Day-17_17_null,NEXUS_5,1.0,en].png} | 0 ...ding-Night-17_18_null,NEXUS_5,1.0,en].png} | 0 ...oserVoice-Day-4_4_null,NEXUS_5,1.0,en].png | 4 +- ...erVoice-Night-4_5_null,NEXUS_5,1.0,en].png | 4 +- 37 files changed, 221 insertions(+), 576 deletions(-) create mode 100644 changelog.d/1784.feature rename libraries/{textcomposer/impl => designsystem}/src/main/res/drawable/ic_pause.xml (100%) rename libraries/{textcomposer/impl => designsystem}/src/main/res/drawable/ic_play.xml (100%) create mode 100644 libraries/designsystem/src/main/res/drawable/ic_stop.xml delete mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt rename libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/{PressEvent.kt => VoiceMessageRecorderEvent.kt} (77%) delete mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressState.kt delete mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateEffects.kt delete mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolder.kt delete mode 100644 libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolderTest.kt delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_HoldToRecordTooltip_null_HoldToRecordTooltip-Day-13_13_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_HoldToRecordTooltip_null_HoldToRecordTooltip-Night-13_14_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_RecordButton_null_RecordButton-Day-12_12_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_RecordButton_null_RecordButton-Night-12_13_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Day-14_14_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Day-12_12_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Night-14_15_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Night-12_13_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Day-15_15_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Day-13_13_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Night-15_16_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Night-13_14_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Day-16_16_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Day-14_14_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Night-16_17_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Night-14_15_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Day-17_17_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Day-15_15_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Night-17_18_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Night-15_16_null,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecorderButton_null_VoiceMessageRecorderButton-Day-16_16_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecorderButton_null_VoiceMessageRecorderButton-Night-16_17_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Day-18_18_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Day-17_17_null,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Night-18_19_null,NEXUS_5,1.0,en].png => ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Night-17_18_null,NEXUS_5,1.0,en].png} (100%) diff --git a/changelog.d/1784.feature b/changelog.d/1784.feature new file mode 100644 index 0000000000..8613d7c798 --- /dev/null +++ b/changelog.d/1784.feature @@ -0,0 +1 @@ +Update voice message recording behaviour. Instead of holding the record button, users can now tap the record button to start recording and tap again to stop recording. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index e34a22eedc..b988d545a6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -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, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt index f80ee15d95..0d384185df 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerEvents.kt @@ -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, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt index d39b065e5a..cd54dc5588 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt @@ -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() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt index 1a6ff988db..8fe1048c3c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt @@ -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 diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_pause.xml b/libraries/designsystem/src/main/res/drawable/ic_pause.xml similarity index 100% rename from libraries/textcomposer/impl/src/main/res/drawable/ic_pause.xml rename to libraries/designsystem/src/main/res/drawable/ic_pause.xml diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_play.xml b/libraries/designsystem/src/main/res/drawable/ic_play.xml similarity index 100% rename from libraries/textcomposer/impl/src/main/res/drawable/ic_play.xml rename to libraries/designsystem/src/main/res/drawable/ic_play.xml diff --git a/libraries/designsystem/src/main/res/drawable/ic_stop.xml b/libraries/designsystem/src/main/res/drawable/ic_stop.xml new file mode 100644 index 0000000000..e4cd1507bb --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_stop.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index fef9d40a5b..9f02254b4d 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -66,7 +66,7 @@ import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.textcomposer.components.ComposerOptionsButton import io.element.android.libraries.textcomposer.components.DismissTextFormattingButton -import io.element.android.libraries.textcomposer.components.RecordButton +import io.element.android.libraries.textcomposer.components.VoiceMessageRecorderButton import io.element.android.libraries.textcomposer.components.SendButton import io.element.android.libraries.textcomposer.components.TextFormatting import io.element.android.libraries.textcomposer.components.VoiceMessageDeleteButton @@ -75,7 +75,7 @@ import io.element.android.libraries.textcomposer.components.VoiceMessageRecordin import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape import io.element.android.libraries.textcomposer.model.Message 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.Suggestion import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent import io.element.android.libraries.textcomposer.model.VoiceMessageState @@ -103,7 +103,7 @@ fun TextComposer( onResetComposerMode: () -> Unit = {}, onAddAttachment: () -> Unit = {}, onDismissTextFormatting: () -> Unit = {}, - onVoiceRecordButtonEvent: (PressEvent) -> Unit = {}, + onVoiceRecorderEvent: (VoiceMessageRecorderEvent) -> Unit = {}, onVoicePlayerEvent: (VoiceMessagePlayerEvent) -> Unit = {}, onSendVoiceMessage: () -> Unit = {}, onDeleteVoiceMessage: () -> Unit = {}, @@ -167,16 +167,15 @@ fun TextComposer( ) } val recordVoiceButton = @Composable { - RecordButton( - onPressStart = { onVoiceRecordButtonEvent(PressEvent.PressStart) }, - onLongPressEnd = { onVoiceRecordButtonEvent(PressEvent.LongPressEnd) }, - onTap = { onVoiceRecordButtonEvent(PressEvent.Tapped) }, + VoiceMessageRecorderButton( + isRecording = voiceMessageState is VoiceMessageState.Recording, + onEvent = onVoiceRecorderEvent, ) } val sendVoiceButton = @Composable { SendButton( canSendMessage = voiceMessageState is VoiceMessageState.Preview, - onClick = { onSendVoiceMessage() }, + onClick = onSendVoiceMessage, composerMode = composerMode, ) } @@ -223,8 +222,12 @@ fun TextComposer( } val voiceDeleteButton = @Composable { - if (voiceMessageState is VoiceMessageState.Preview) { - VoiceMessageDeleteButton(enabled = !voiceMessageState.isSending, onClick = onDeleteVoiceMessage) + when (voiceMessageState) { + is VoiceMessageState.Preview -> + VoiceMessageDeleteButton(enabled = !voiceMessageState.isSending, onClick = onDeleteVoiceMessage) + is VoiceMessageState.Recording -> + VoiceMessageDeleteButton(enabled = true, onClick = { onVoiceRecorderEvent(VoiceMessageRecorderEvent.Cancel) }) + else -> {} } } @@ -286,7 +289,7 @@ private fun StandardLayout( verticalAlignment = Alignment.Bottom, ) { if (enableVoiceMessages && voiceMessageState !is VoiceMessageState.Idle) { - if (voiceMessageState is VoiceMessageState.Preview) { + if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) { Box( modifier = Modifier .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt deleted file mode 100644 index 0a0710095e..0000000000 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/RecordButton.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (c) 2023 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. - */ - -@file:OptIn(ExperimentalMaterial3Api::class) - -package io.element.android.libraries.textcomposer.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TooltipState -import androidx.compose.material3.rememberTooltipState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.components.tooltip.ElementTooltipDefaults -import io.element.android.libraries.designsystem.components.tooltip.PlainTooltip -import io.element.android.libraries.designsystem.components.tooltip.TooltipBox -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.IconButton -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.textcomposer.R -import io.element.android.libraries.textcomposer.utils.PressState -import io.element.android.libraries.textcomposer.utils.PressStateEffects -import io.element.android.libraries.textcomposer.utils.rememberPressState -import io.element.android.libraries.theme.ElementTheme -import io.element.android.libraries.ui.strings.CommonStrings -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -internal fun RecordButton( - modifier: Modifier = Modifier, - initialTooltipIsVisible: Boolean = false, - onPressStart: () -> Unit = {}, - onLongPressEnd: () -> Unit = {}, - onTap: () -> Unit = {}, -) { - val coroutineScope = rememberCoroutineScope() - val pressState = rememberPressState() - val hapticFeedback = LocalHapticFeedback.current - - val performHapticFeedback = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - } - - val tooltipState = rememberTooltipState( - initialIsVisible = initialTooltipIsVisible - ) - - PressStateEffects( - pressState = pressState.value, - onPressStart = { - onPressStart() - performHapticFeedback() - }, - onLongPressEnd = { - onLongPressEnd() - performHapticFeedback() - }, - onTap = { - onTap() - performHapticFeedback() - coroutineScope.launch { tooltipState.show() } - }, - ) - Box(modifier = modifier) { - HoldToRecordTooltip( - tooltipState = tooltipState, - spacingBetweenTooltipAndAnchor = 0.dp, // Accounts for the 48.dp size of the record button - anchor = { - RecordButtonView( - isPressed = pressState.value is PressState.Pressing, - modifier = Modifier - .pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val event = awaitPointerEvent() - coroutineScope.launch { - when (event.type) { - PointerEventType.Press -> pressState.press() - PointerEventType.Release -> pressState.release() - } - } - } - } - } - ) - } - ) - } -} - -@Composable -private fun RecordButtonView( - isPressed: Boolean, - modifier: Modifier = Modifier, -) { - IconButton( - modifier = modifier - .size(48.dp), - onClick = {}, - ) { - Icon( - modifier = Modifier.size(24.dp), - resourceId = if (isPressed) { - CommonDrawables.ic_compound_mic_on_solid - } else { - CommonDrawables.ic_compound_mic_on_outline - }, - contentDescription = stringResource(CommonStrings.a11y_voice_message_record), - tint = ElementTheme.colors.iconSecondary, - ) - } -} - -@Composable -private fun HoldToRecordTooltip( - tooltipState: TooltipState, - spacingBetweenTooltipAndAnchor: Dp, - modifier: Modifier = Modifier, - anchor: @Composable () -> Unit, -) { - TooltipBox( - positionProvider = ElementTooltipDefaults.rememberPlainTooltipPositionProvider( - spacingBetweenTooltipAndAnchor = spacingBetweenTooltipAndAnchor, - ), - tooltip = { - PlainTooltip { - Text( - text = stringResource(R.string.screen_room_voice_message_tooltip), - color = ElementTheme.colors.textOnSolidPrimary, - style = ElementTheme.typography.fontBodySmMedium, - ) - } - }, - state = tooltipState, - modifier = modifier, - focusable = false, - enableUserInput = false, - content = anchor, - ) -} - -@PreviewsDayNight -@Composable -internal fun RecordButtonPreview() = ElementPreview { - Row { - RecordButtonView(isPressed = false) - RecordButtonView(isPressed = true) - } -} - -@PreviewsDayNight -@Composable -internal fun HoldToRecordTooltipPreview() = ElementPreview { - Box(modifier = Modifier.fillMaxSize()) { - RecordButton( - modifier = Modifier.align(Alignment.BottomEnd), - initialTooltipIsVisible = true, - ) - } -} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index 222ee2aeaf..bcf307ec2e 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -43,7 +43,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.textcomposer.R +import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.utils.time.formatShort @@ -145,14 +145,14 @@ private fun PlayerButton( @Composable private fun PauseIcon() = Icon( - resourceId = R.drawable.ic_pause, + resourceId = CommonDrawables.ic_pause, contentDescription = stringResource(id = CommonStrings.a11y_pause), modifier = Modifier.size(20.dp), ) @Composable private fun PlayIcon() = Icon( - resourceId = R.drawable.ic_play, + resourceId = CommonDrawables.ic_play, contentDescription = stringResource(id = CommonStrings.a11y_play), modifier = Modifier.size(20.dp), ) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt new file mode 100644 index 0000000000..d2a8c2cae1 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023 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 io.element.android.libraries.textcomposer.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.utils.CommonDrawables +import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun VoiceMessageRecorderButton( + isRecording: Boolean, + modifier: Modifier = Modifier, + onEvent: (VoiceMessageRecorderEvent) -> Unit = {}, +) { + val hapticFeedback = LocalHapticFeedback.current + + val performHapticFeedback = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + } + + if (isRecording) { + StopButton( + modifier = modifier, + onClick = { + performHapticFeedback() + onEvent(VoiceMessageRecorderEvent.Stop) + } + ) + } else { + StartButton( + modifier = modifier, + onClick = { + performHapticFeedback() + onEvent(VoiceMessageRecorderEvent.Start) + } + ) + } +} + +@Composable +private fun StartButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) = IconButton( + modifier = modifier.size(48.dp), + onClick = onClick, +) { + Icon( + modifier = Modifier.size(24.dp), + resourceId = CommonDrawables.ic_compound_mic_on_outline, + contentDescription = stringResource(CommonStrings.a11y_voice_message_record), + tint = ElementTheme.colors.iconSecondary, + ) +} + +@Composable +private fun StopButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) = IconButton( + modifier = modifier + .size(48.dp), + onClick = onClick, +) { + Box( + Modifier + .size(36.dp) + .background( + color = ElementTheme.colors.bgActionPrimaryRest, + shape = CircleShape, + ) + ) + Icon( + modifier = Modifier.size(24.dp), + resourceId = CommonDrawables.ic_stop, + contentDescription = stringResource(CommonStrings.a11y_voice_message_stop_recording), + tint = ElementTheme.colors.iconOnSolidPrimary, + ) +} + +@PreviewsDayNight +@Composable +internal fun VoiceMessageRecorderButtonPreview() = ElementPreview { + Row { + VoiceMessageRecorderButton(isRecording = false) + VoiceMessageRecorderButton(isRecording = true) + } +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/PressEvent.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageRecorderEvent.kt similarity index 77% rename from libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/PressEvent.kt rename to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageRecorderEvent.kt index 340540886d..17091930c5 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/PressEvent.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageRecorderEvent.kt @@ -16,8 +16,8 @@ package io.element.android.libraries.textcomposer.model -sealed interface PressEvent { - data object PressStart: PressEvent - data object Tapped: PressEvent - data object LongPressEnd: PressEvent +sealed interface VoiceMessageRecorderEvent { + data object Start: VoiceMessageRecorderEvent + data object Stop: VoiceMessageRecorderEvent + data object Cancel: VoiceMessageRecorderEvent } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressState.kt deleted file mode 100644 index 50df6d591c..0000000000 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressState.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2023 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 io.element.android.libraries.textcomposer.utils - -/** - * State of a press gesture. - */ -internal sealed interface PressState { - data class Idle( - val lastPress: Pressing? - ) : PressState - - sealed interface Pressing : PressState - data object Tapping : Pressing - data object LongPressing : Pressing -} - diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateEffects.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateEffects.kt deleted file mode 100644 index aaee6bae0f..0000000000 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateEffects.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2023 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 io.element.android.libraries.textcomposer.utils - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect - -/** - * React to [PressState] changes. - */ -@Composable -internal fun PressStateEffects( - pressState: PressState, - onPressStart: () -> Unit = {}, - onLongPressStart: () -> Unit = {}, - onTap: () -> Unit = {}, - onLongPressEnd: () -> Unit = {}, -) { - LaunchedEffect(pressState) { - when (pressState) { - is PressState.Idle -> - when (pressState.lastPress) { - PressState.Tapping -> onTap() - PressState.LongPressing -> onLongPressEnd() - null -> {} // Do nothing - } - is PressState.LongPressing -> onLongPressStart() - PressState.Tapping -> onPressStart() - } - } -} - - diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolder.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolder.kt deleted file mode 100644 index 7021b8ac46..0000000000 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolder.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2023 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 io.element.android.libraries.textcomposer.utils - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalViewConfiguration -import kotlinx.coroutines.Job -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.yield -import timber.log.Timber - -@Composable -internal fun rememberPressState( - longPressTimeoutMillis: Long = LocalViewConfiguration.current.longPressTimeoutMillis, -): PressStateHolder { - return remember(longPressTimeoutMillis) { - PressStateHolder(longPressTimeoutMillis = longPressTimeoutMillis) - } -} - -/** - * State machine that keeps track of the pressed state. - * - * When a press is started, the state will transition through: - * [PressState.Idle] -> [PressState.Tapping] -> ... - * - * If a press is held for a longer time, the state will continue through: - * ... -> [PressState.LongPressing] -> ... - * - * When the press is released the states will then transition back to idle. - * ... -> [PressState.Idle] - * - * Whether a press should be considered a tap or a long press can be determined by - * looking at the last press when in the idle state. - * - * @see [PressStateEffects] - * @see [rememberPressState] - */ -internal class PressStateHolder( - private val longPressTimeoutMillis: Long, -) : State { - private var state: PressState by mutableStateOf(PressState.Idle(lastPress = null)) - - override val value: PressState - get() = state - - private var longPressTimer: Job? = null - - suspend fun press() = coroutineScope { - when (state) { - is PressState.Idle -> { - state = PressState.Tapping - } - is PressState.Pressing -> - Timber.e("Pointer pressed but it has not been released") - } - - longPressTimer = launch { - delay(longPressTimeoutMillis) - yield() - - if (isActive && state == PressState.Tapping) { - state = PressState.LongPressing - } - } - } - - fun release() { - longPressTimer?.cancel() - longPressTimer = null - when (val lastState = state) { - is PressState.Pressing -> - state = PressState.Idle(lastPress = lastState) - is PressState.Idle -> - Timber.e("Pointer pressed but it has not been released") - } - } -} - diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolderTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolderTest.kt deleted file mode 100644 index 615692911e..0000000000 --- a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/utils/PressStateHolderTest.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) 2023 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 io.element.android.libraries.textcomposer.utils - -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.textcomposer.utils.PressState.Idle -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.async -import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runTest -import org.junit.Test -import kotlin.time.Duration.Companion.milliseconds - -@OptIn(ExperimentalCoroutinesApi::class) class PressStateHolderTest { - companion object { - const val LONG_PRESS_TIMEOUT_MILLIS = 1L - } - @Test - fun `it starts in idle state`() = runTest { - val stateHolder = createStateHolder() - assertThat(stateHolder.value).isEqualTo(Idle(lastPress = null)) - } - - @Test - fun `when press, it moves to tapping state`() = runTest { - val stateHolder = createStateHolder() - val press = async { stateHolder.press() } - advanceTimeBy(1.milliseconds) - assertThat(stateHolder.value).isEqualTo(PressState.Tapping) - press.await() - } - - @Test - fun `when release after short delay, it moves through tap states`() = runTest { - val stateHolder = createStateHolder() - val press = async { stateHolder.press() } - advanceTimeBy(1.milliseconds) - assertThat(stateHolder.value).isEqualTo(PressState.Tapping) - stateHolder.release() - advanceTimeBy(1.milliseconds) // wait for the long press timeout which should not be triggered - assertThat(stateHolder.value).isEqualTo(Idle(lastPress = PressState.Tapping)) - press.await() - } - - @Test - fun `when hold, it moves through long press states`() = runTest { - val stateHolder = createStateHolder() - val press = async { stateHolder.press() } - advanceTimeBy(1.milliseconds) - assertThat(stateHolder.value).isEqualTo(PressState.Tapping) - advanceTimeBy(1.milliseconds) - assertThat(stateHolder.value).isEqualTo(PressState.LongPressing) - stateHolder.release() - assertThat(stateHolder.value).isEqualTo(Idle(lastPress = PressState.LongPressing)) - press.await() - } - - @Test - fun `when release and repress, it doesn't enter long press states`() = runTest { - val stateHolder = createStateHolder() - val press1 = async { stateHolder.press() } - advanceTimeBy(1.milliseconds) - assertThat(stateHolder.value).isEqualTo(PressState.Tapping) - stateHolder.release() - val press2 = async { stateHolder.press() } - advanceTimeBy(1.milliseconds) - assertThat(stateHolder.value).isEqualTo(PressState.Tapping) - press1.await() - press2.await() - } - - @Test - fun `when press twice without releasing, it doesn't throw an error`() = runTest { - val stateHolder = createStateHolder() - stateHolder.press() - stateHolder.press() - } - - @Test - fun `when release without first pressing, it doesn't throw an error`() = runTest { - val stateHolder = createStateHolder() - stateHolder.release() - } - - @Test - fun `when release twice without pressing, it doesn't throw an error `() = runTest { - val stateHolder = createStateHolder() - stateHolder.press() - stateHolder.release() - stateHolder.release() - } - - private fun createStateHolder() = - PressStateHolder( - LONG_PRESS_TIMEOUT_MILLIS, - ) -} diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_MessageComposerViewVoice_null_MessageComposerViewVoice-Day-6_6_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_MessageComposerViewVoice_null_MessageComposerViewVoice-Day-6_6_null_0,NEXUS_5,1.0,en].png index 3ec6004b6d..521717b05b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_MessageComposerViewVoice_null_MessageComposerViewVoice-Day-6_6_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_MessageComposerViewVoice_null_MessageComposerViewVoice-Day-6_6_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d5209087d7841a80f7213f26aaf8322cc5fee03466b9059e06b0daf5f489c1b -size 10012 +oid sha256:5cd4e624c0ed8abe5d22d60992f64f81e91fa41ea7d8772acc5e3832eadbcbb7 +size 10581 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_MessageComposerViewVoice_null_MessageComposerViewVoice-Night-6_7_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_MessageComposerViewVoice_null_MessageComposerViewVoice-Night-6_7_null_0,NEXUS_5,1.0,en].png index dcddb1ae01..3dd0888ff4 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_MessageComposerViewVoice_null_MessageComposerViewVoice-Night-6_7_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_MessageComposerViewVoice_null_MessageComposerViewVoice-Night-6_7_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b8fca1de81e424f01a518e3a2547b58c0e9eb7249327f3fcdb1cf62ab655972 -size 9429 +oid sha256:01eda47acbf273bf61e60dbbc466aac5e9a4eac7690af4cde2e06e7e1f190bcf +size 10152 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_HoldToRecordTooltip_null_HoldToRecordTooltip-Day-13_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_HoldToRecordTooltip_null_HoldToRecordTooltip-Day-13_13_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 2f3dc97e8c..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_HoldToRecordTooltip_null_HoldToRecordTooltip-Day-13_13_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e6d75686b1463d11d89e5130b160c3c446f26612e8da44981cc6994687001d80 -size 7093 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_HoldToRecordTooltip_null_HoldToRecordTooltip-Night-13_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_HoldToRecordTooltip_null_HoldToRecordTooltip-Night-13_14_null,NEXUS_5,1.0,en].png deleted file mode 100644 index ff6633e96a..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_HoldToRecordTooltip_null_HoldToRecordTooltip-Night-13_14_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f8c96b9ce6a8d9136c56789e9e6af25b4259143f6416305c523f4e5224bc8fa8 -size 5898 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_RecordButton_null_RecordButton-Day-12_12_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_RecordButton_null_RecordButton-Day-12_12_null,NEXUS_5,1.0,en].png deleted file mode 100644 index f0c42b1810..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_RecordButton_null_RecordButton-Day-12_12_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0e8a1efaf22dd86e6e27904d72a820cfb0b5d1d38ffeb5745bfa6ca3b0a1c85 -size 6003 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_RecordButton_null_RecordButton-Night-12_13_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_RecordButton_null_RecordButton-Night-12_13_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 27368109ad..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_RecordButton_null_RecordButton-Night-12_13_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f2db2983459eae8b7538c51d125836807b29334b01fbca495c3c9630fb510c2 -size 5969 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Day-14_14_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Day-12_12_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Day-14_14_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Day-12_12_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Night-14_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Night-12_13_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Night-14_15_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_SendButton_null_SendButton-Night-12_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Day-15_15_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Day-13_13_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Day-15_15_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Day-13_13_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Night-15_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Night-13_14_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Night-15_16_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_TextFormatting_null_TextFormatting-Night-13_14_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Day-16_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Day-14_14_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Day-16_16_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Day-14_14_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Night-16_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Night-14_15_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Night-16_17_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageDeleteButton_null_VoiceMessageDeleteButton-Night-14_15_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Day-17_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Day-15_15_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Day-17_17_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Day-15_15_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Night-17_18_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Night-15_16_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Night-17_18_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessagePreview_null_VoiceMessagePreview-Night-15_16_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecorderButton_null_VoiceMessageRecorderButton-Day-16_16_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecorderButton_null_VoiceMessageRecorderButton-Day-16_16_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3059b1e5ad --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecorderButton_null_VoiceMessageRecorderButton-Day-16_16_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b6f6caef8d902d4e0b93721f212dcd3f20cde267e1782813f0c55498bca5c8e +size 6805 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecorderButton_null_VoiceMessageRecorderButton-Night-16_17_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecorderButton_null_VoiceMessageRecorderButton-Night-16_17_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..279ced000a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecorderButton_null_VoiceMessageRecorderButton-Night-16_17_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6def3059e37f5c7d6d8930df229a895f5d37b95f46a138fa4ef790bbf76b80b6 +size 6744 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Day-18_18_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Day-17_17_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Day-18_18_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Day-17_17_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Night-18_19_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Night-17_18_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Night-18_19_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_VoiceMessageRecording_null_VoiceMessageRecording-Night-17_18_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerVoice_null_TextComposerVoice-Day-4_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerVoice_null_TextComposerVoice-Day-4_4_null,NEXUS_5,1.0,en].png index f5ba129fd3..e9254642a2 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerVoice_null_TextComposerVoice-Day-4_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerVoice_null_TextComposerVoice-Day-4_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:991416dcd21ebf8e2d57baac7cb42506596a1a346dd7d643b09c84c473e053b7 -size 28016 +oid sha256:9a76e1dcbc4461b6e0a1cca836a097846262a3d302befbec4486ed7ee837a8da +size 28214 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerVoice_null_TextComposerVoice-Night-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerVoice_null_TextComposerVoice-Night-4_5_null,NEXUS_5,1.0,en].png index 8bd3ded996..304d6dc23e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerVoice_null_TextComposerVoice-Night-4_5_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_TextComposerVoice_null_TextComposerVoice-Night-4_5_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8b8c60473ce1b9e7451e763c960a9242bb83ca1d3769ee921db830ff6e57e7c -size 27094 +oid sha256:25be7fa172232c508c58369f60e82fc7ec1d7362d9f701c1e698ec8d8498b482 +size 27449