Browse Source

Add time to voice message composer UI (#1720)

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
pull/1712/head
jonnyandrew 11 months ago committed by GitHub
parent
commit
83a6395688
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt
  2. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt
  3. 21
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
  4. 7
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt
  5. 20
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt
  6. 1
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt
  7. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png
  8. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png
  9. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png
  10. BIN
      tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png

6
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPlayer.kt

@ -100,6 +100,10 @@ class VoiceMessageComposerPlayer @Inject constructor( @@ -100,6 +100,10 @@ class VoiceMessageComposerPlayer @Inject constructor(
* The progress of this player between 0 and 1.
*/
val progress: Float =
if (duration <= currentPosition) 0f else currentPosition.toFloat() / duration.toFloat()
if (duration == 0L)
0f
else
(currentPosition.toFloat() / duration.toFloat())
.coerceAtMost(1f) // Current position may exceed reported duration
}
}

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenter.kt

@ -50,6 +50,7 @@ import kotlinx.coroutines.launch @@ -50,6 +50,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@SingleIn(RoomScope::class)
class VoiceMessageComposerPresenter @Inject constructor(
@ -191,6 +192,7 @@ class VoiceMessageComposerPresenter @Inject constructor( @@ -191,6 +192,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
isSending = isSending,
isPlaying = isPlaying,
playbackProgress = playerState.progress,
time = playerState.currentPosition.milliseconds,
waveform = waveform,
)
else -> VoiceMessageState.Idle

21
features/messages/impl/src/test/kotlin/io/element/android/features/messages/voicemessages/composer/VoiceMessageComposerPresenterTest.kt

@ -50,13 +50,14 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState @@ -50,13 +50,14 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
class VoiceMessageComposerPresenterTest {
@ -195,7 +196,7 @@ class VoiceMessageComposerPresenterTest { @@ -195,7 +196,7 @@ class VoiceMessageComposerPresenterTest {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
val finalState = awaitItem().also {
assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = true, playbackProgress = 0.1f))
assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = true, playbackProgress = 0.1f, time = RECORDING_DURATION))
}
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0)
@ -214,7 +215,7 @@ class VoiceMessageComposerPresenterTest { @@ -214,7 +215,7 @@ class VoiceMessageComposerPresenterTest {
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Pause))
val finalState = awaitItem().also {
assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = false, playbackProgress = 0.1f))
assertThat(it.voiceMessageState).isEqualTo(aPreviewState(isPlaying = false, playbackProgress = 0.1f, time = RECORDING_DURATION))
}
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0)
@ -251,7 +252,7 @@ class VoiceMessageComposerPresenterTest { @@ -251,7 +252,7 @@ class VoiceMessageComposerPresenterTest {
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage)
awaitItem().apply {
assertThat(voiceMessageState).isEqualTo(aPreviewState(isPlaying = false, playbackProgress = 0.1f))
assertThat(voiceMessageState).isEqualTo(aPreviewState(isPlaying = false, playbackProgress = 0.1f, time = RECORDING_DURATION))
}
val finalState = awaitItem()
@ -321,9 +322,11 @@ class VoiceMessageComposerPresenterTest { @@ -321,9 +322,11 @@ class VoiceMessageComposerPresenterTest {
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState(
isSending = true, isPlaying = false, playbackProgress = 0.1f
))
assertThat(awaitItem().voiceMessageState).isEqualTo(
aPreviewState(
isSending = true, isPlaying = false, playbackProgress = 0.1f, time = RECORDING_DURATION
)
)
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
@ -570,7 +573,7 @@ class VoiceMessageComposerPresenterTest { @@ -570,7 +573,7 @@ class VoiceMessageComposerPresenterTest {
is VoiceMessageState.Preview -> when (state.isPlaying) {
// If the preview was playing, it pauses
true -> awaitItem().apply {
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.1f))
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.1f, time = RECORDING_DURATION))
}
false -> mostRecentState
}
@ -624,11 +627,13 @@ class VoiceMessageComposerPresenterTest { @@ -624,11 +627,13 @@ class VoiceMessageComposerPresenterTest {
isPlaying: Boolean = false,
playbackProgress: Float = 0f,
isSending: Boolean = false,
time: Duration = 0.seconds,
waveform: List<Float> = voiceRecorder.waveform,
) = VoiceMessageState.Preview(
isPlaying = isPlaying,
playbackProgress = playbackProgress,
isSending = isSending,
time = time,
waveform = waveform.toImmutableList(),
)
}

7
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt

@ -85,7 +85,6 @@ import io.element.android.wysiwyg.compose.RichTextEditor @@ -85,7 +85,6 @@ import io.element.android.wysiwyg.compose.RichTextEditor
import io.element.android.wysiwyg.compose.RichTextEditorState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import uniffi.wysiwyg_composer.MenuAction
import kotlin.time.Duration.Companion.seconds
@ -211,6 +210,7 @@ fun TextComposer( @@ -211,6 +210,7 @@ fun TextComposer(
isPlaying = voiceMessageState.isPlaying,
waveform = voiceMessageState.waveform,
playbackProgress = voiceMessageState.playbackProgress,
time = voiceMessageState.time,
onPlayClick = onPlayVoiceMessageClicked,
onPauseClick = onPauseVoiceMessageClicked,
onSeek = onSeekVoiceMessage,
@ -816,13 +816,14 @@ internal fun TextComposerVoicePreview() = ElementPreview { @@ -816,13 +816,14 @@ internal fun TextComposerVoicePreview() = ElementPreview {
enableVoiceMessages = true,
)
PreviewColumn(items = persistentListOf({
VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, List(100) { it.toFloat() / 100 }.toPersistentList()))
VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, createFakeWaveform()))
}, {
VoicePreview(
voiceMessageState = VoiceMessageState.Preview(
isSending = false,
isPlaying = false,
waveform = createFakeWaveform(),
time = 0.seconds,
playbackProgress = 0.0f
)
)
@ -832,6 +833,7 @@ internal fun TextComposerVoicePreview() = ElementPreview { @@ -832,6 +833,7 @@ internal fun TextComposerVoicePreview() = ElementPreview {
isSending = false,
isPlaying = true,
waveform = createFakeWaveform(),
time = 3.seconds,
playbackProgress = 0.2f
)
)
@ -841,6 +843,7 @@ internal fun TextComposerVoicePreview() = ElementPreview { @@ -841,6 +843,7 @@ internal fun TextComposerVoicePreview() = ElementPreview {
isSending = true,
isPlaying = false,
waveform = createFakeWaveform(),
time = 61.seconds,
playbackProgress = 0.0f
)
)

20
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt

@ -34,6 +34,7 @@ import androidx.compose.runtime.Composable @@ -34,6 +34,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
import io.element.android.libraries.designsystem.components.media.createFakeWaveform
@ -42,16 +43,21 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -42,16 +43,21 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.applyScaleUp
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.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.time.formatShort
import kotlinx.collections.immutable.ImmutableList
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@Composable
internal fun VoiceMessagePreview(
isInteractive: Boolean,
isPlaying: Boolean,
waveform: ImmutableList<Float>,
time: Duration,
modifier: Modifier = Modifier,
playbackProgress: Float = 0f,
onPlayClick: () -> Unit = {},
@ -85,7 +91,13 @@ internal fun VoiceMessagePreview( @@ -85,7 +91,13 @@ internal fun VoiceMessagePreview(
Spacer(modifier = Modifier.width(8.dp))
// TODO Add timer UI
Text(
text = time.formatShort(),
color = ElementTheme.materialColors.secondary,
style = ElementTheme.typography.fontBodySmMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = Modifier.width(12.dp))
@ -151,8 +163,8 @@ internal fun VoiceMessagePreviewPreview() = ElementPreview { @@ -151,8 +163,8 @@ internal fun VoiceMessagePreviewPreview() = ElementPreview {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
VoiceMessagePreview(isInteractive = true, isPlaying = true, waveform = createFakeWaveform())
VoiceMessagePreview(isInteractive = true, isPlaying = false, waveform = createFakeWaveform())
VoiceMessagePreview(isInteractive = false, isPlaying = false, waveform = createFakeWaveform())
VoiceMessagePreview(isInteractive = true, isPlaying = true, time = 2.seconds, playbackProgress = 0.2f, waveform = createFakeWaveform())
VoiceMessagePreview(isInteractive = true, isPlaying = false, time = 0.seconds, playbackProgress = 0.0f, waveform = createFakeWaveform())
VoiceMessagePreview(isInteractive = false, isPlaying = false, time = 789.seconds, playbackProgress = 0.0f, waveform = createFakeWaveform())
}
}

1
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/VoiceMessageState.kt

@ -26,6 +26,7 @@ sealed class VoiceMessageState { @@ -26,6 +26,7 @@ sealed class VoiceMessageState {
val isSending: Boolean,
val isPlaying: Boolean,
val playbackProgress: Float,
val time: Duration,
val waveform: ImmutableList<Float>,
): VoiceMessageState()

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-D-16_16_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer.components_null_VoiceMessagePreview-N-16_17_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-D-4_4_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerVoice-N-4_5_null,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save