Add time to voice message composer UI (#1720)
--------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
b7e6375ac3
commit
3e58e03a10
10 changed files with 50 additions and 23 deletions
|
|
@ -100,6 +100,10 @@ class VoiceMessageComposerPlayer @Inject constructor(
|
||||||
* The progress of this player between 0 and 1.
|
* The progress of this player between 0 and 1.
|
||||||
*/
|
*/
|
||||||
val progress: Float =
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@SingleIn(RoomScope::class)
|
@SingleIn(RoomScope::class)
|
||||||
class VoiceMessageComposerPresenter @Inject constructor(
|
class VoiceMessageComposerPresenter @Inject constructor(
|
||||||
|
|
@ -191,6 +192,7 @@ class VoiceMessageComposerPresenter @Inject constructor(
|
||||||
isSending = isSending,
|
isSending = isSending,
|
||||||
isPlaying = isPlaying,
|
isPlaying = isPlaying,
|
||||||
playbackProgress = playerState.progress,
|
playbackProgress = playerState.progress,
|
||||||
|
time = playerState.currentPosition.milliseconds,
|
||||||
waveform = waveform,
|
waveform = waveform,
|
||||||
)
|
)
|
||||||
else -> VoiceMessageState.Idle
|
else -> VoiceMessageState.Idle
|
||||||
|
|
|
||||||
|
|
@ -50,13 +50,14 @@ import io.element.android.libraries.textcomposer.model.VoiceMessageState
|
||||||
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
|
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class VoiceMessageComposerPresenterTest {
|
class VoiceMessageComposerPresenterTest {
|
||||||
|
|
@ -195,7 +196,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||||
val finalState = awaitItem().also {
|
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)
|
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0)
|
||||||
|
|
||||||
|
|
@ -214,7 +215,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Pause))
|
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Pause))
|
||||||
val finalState = awaitItem().also {
|
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)
|
voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0)
|
||||||
|
|
||||||
|
|
@ -251,7 +252,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage)
|
awaitItem().eventSink(VoiceMessageComposerEvents.DeleteVoiceMessage)
|
||||||
awaitItem().apply {
|
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()
|
val finalState = awaitItem()
|
||||||
|
|
@ -321,9 +322,11 @@ class VoiceMessageComposerPresenterTest {
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
|
awaitItem().eventSink(VoiceMessageComposerEvents.RecordButtonEvent(PressEvent.LongPressEnd))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
awaitItem().eventSink(VoiceMessageComposerEvents.PlayerEvent(VoiceMessagePlayerEvent.Play))
|
||||||
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
awaitItem().eventSink(VoiceMessageComposerEvents.SendVoiceMessage)
|
||||||
assertThat(awaitItem().voiceMessageState).isEqualTo(aPreviewState(
|
assertThat(awaitItem().voiceMessageState).isEqualTo(
|
||||||
isSending = true, isPlaying = false, playbackProgress = 0.1f
|
aPreviewState(
|
||||||
))
|
isSending = true, isPlaying = false, playbackProgress = 0.1f, time = RECORDING_DURATION
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle)
|
||||||
|
|
@ -570,7 +573,7 @@ class VoiceMessageComposerPresenterTest {
|
||||||
is VoiceMessageState.Preview -> when (state.isPlaying) {
|
is VoiceMessageState.Preview -> when (state.isPlaying) {
|
||||||
// If the preview was playing, it pauses
|
// If the preview was playing, it pauses
|
||||||
true -> awaitItem().apply {
|
true -> awaitItem().apply {
|
||||||
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.1f))
|
assertThat(voiceMessageState).isEqualTo(aPreviewState(playbackProgress = 0.1f, time = RECORDING_DURATION))
|
||||||
}
|
}
|
||||||
false -> mostRecentState
|
false -> mostRecentState
|
||||||
}
|
}
|
||||||
|
|
@ -624,11 +627,13 @@ class VoiceMessageComposerPresenterTest {
|
||||||
isPlaying: Boolean = false,
|
isPlaying: Boolean = false,
|
||||||
playbackProgress: Float = 0f,
|
playbackProgress: Float = 0f,
|
||||||
isSending: Boolean = false,
|
isSending: Boolean = false,
|
||||||
|
time: Duration = 0.seconds,
|
||||||
waveform: List<Float> = voiceRecorder.waveform,
|
waveform: List<Float> = voiceRecorder.waveform,
|
||||||
) = VoiceMessageState.Preview(
|
) = VoiceMessageState.Preview(
|
||||||
isPlaying = isPlaying,
|
isPlaying = isPlaying,
|
||||||
playbackProgress = playbackProgress,
|
playbackProgress = playbackProgress,
|
||||||
isSending = isSending,
|
isSending = isSending,
|
||||||
|
time = time,
|
||||||
waveform = waveform.toImmutableList(),
|
waveform = waveform.toImmutableList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,6 @@ import io.element.android.wysiwyg.compose.RichTextEditor
|
||||||
import io.element.android.wysiwyg.compose.RichTextEditorState
|
import io.element.android.wysiwyg.compose.RichTextEditorState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
|
||||||
import uniffi.wysiwyg_composer.MenuAction
|
import uniffi.wysiwyg_composer.MenuAction
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
@ -211,6 +210,7 @@ fun TextComposer(
|
||||||
isPlaying = voiceMessageState.isPlaying,
|
isPlaying = voiceMessageState.isPlaying,
|
||||||
waveform = voiceMessageState.waveform,
|
waveform = voiceMessageState.waveform,
|
||||||
playbackProgress = voiceMessageState.playbackProgress,
|
playbackProgress = voiceMessageState.playbackProgress,
|
||||||
|
time = voiceMessageState.time,
|
||||||
onPlayClick = onPlayVoiceMessageClicked,
|
onPlayClick = onPlayVoiceMessageClicked,
|
||||||
onPauseClick = onPauseVoiceMessageClicked,
|
onPauseClick = onPauseVoiceMessageClicked,
|
||||||
onSeek = onSeekVoiceMessage,
|
onSeek = onSeekVoiceMessage,
|
||||||
|
|
@ -816,13 +816,14 @@ internal fun TextComposerVoicePreview() = ElementPreview {
|
||||||
enableVoiceMessages = true,
|
enableVoiceMessages = true,
|
||||||
)
|
)
|
||||||
PreviewColumn(items = persistentListOf({
|
PreviewColumn(items = persistentListOf({
|
||||||
VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, List(100) { it.toFloat() / 100 }.toPersistentList()))
|
VoicePreview(voiceMessageState = VoiceMessageState.Recording(61.seconds, createFakeWaveform()))
|
||||||
}, {
|
}, {
|
||||||
VoicePreview(
|
VoicePreview(
|
||||||
voiceMessageState = VoiceMessageState.Preview(
|
voiceMessageState = VoiceMessageState.Preview(
|
||||||
isSending = false,
|
isSending = false,
|
||||||
isPlaying = false,
|
isPlaying = false,
|
||||||
waveform = createFakeWaveform(),
|
waveform = createFakeWaveform(),
|
||||||
|
time = 0.seconds,
|
||||||
playbackProgress = 0.0f
|
playbackProgress = 0.0f
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -832,6 +833,7 @@ internal fun TextComposerVoicePreview() = ElementPreview {
|
||||||
isSending = false,
|
isSending = false,
|
||||||
isPlaying = true,
|
isPlaying = true,
|
||||||
waveform = createFakeWaveform(),
|
waveform = createFakeWaveform(),
|
||||||
|
time = 3.seconds,
|
||||||
playbackProgress = 0.2f
|
playbackProgress = 0.2f
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -841,6 +843,7 @@ internal fun TextComposerVoicePreview() = ElementPreview {
|
||||||
isSending = true,
|
isSending = true,
|
||||||
isPlaying = false,
|
isPlaying = false,
|
||||||
waveform = createFakeWaveform(),
|
waveform = createFakeWaveform(),
|
||||||
|
time = 61.seconds,
|
||||||
playbackProgress = 0.0f
|
playbackProgress = 0.0f
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
||||||
import io.element.android.libraries.designsystem.components.media.createFakeWaveform
|
import io.element.android.libraries.designsystem.components.media.createFakeWaveform
|
||||||
|
|
@ -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.text.applyScaleUp
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
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.IconButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.textcomposer.R
|
import io.element.android.libraries.textcomposer.R
|
||||||
import io.element.android.libraries.theme.ElementTheme
|
import io.element.android.libraries.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import io.element.android.libraries.ui.utils.time.formatShort
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun VoiceMessagePreview(
|
internal fun VoiceMessagePreview(
|
||||||
isInteractive: Boolean,
|
isInteractive: Boolean,
|
||||||
isPlaying: Boolean,
|
isPlaying: Boolean,
|
||||||
waveform: ImmutableList<Float>,
|
waveform: ImmutableList<Float>,
|
||||||
|
time: Duration,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
playbackProgress: Float = 0f,
|
playbackProgress: Float = 0f,
|
||||||
onPlayClick: () -> Unit = {},
|
onPlayClick: () -> Unit = {},
|
||||||
|
|
@ -85,7 +91,13 @@ internal fun VoiceMessagePreview(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
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))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
|
||||||
|
|
@ -151,8 +163,8 @@ internal fun VoiceMessagePreviewPreview() = ElementPreview {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
VoiceMessagePreview(isInteractive = true, isPlaying = true, waveform = createFakeWaveform())
|
VoiceMessagePreview(isInteractive = true, isPlaying = true, time = 2.seconds, playbackProgress = 0.2f, waveform = createFakeWaveform())
|
||||||
VoiceMessagePreview(isInteractive = true, isPlaying = false, waveform = createFakeWaveform())
|
VoiceMessagePreview(isInteractive = true, isPlaying = false, time = 0.seconds, playbackProgress = 0.0f, waveform = createFakeWaveform())
|
||||||
VoiceMessagePreview(isInteractive = false, isPlaying = false, waveform = createFakeWaveform())
|
VoiceMessagePreview(isInteractive = false, isPlaying = false, time = 789.seconds, playbackProgress = 0.0f, waveform = createFakeWaveform())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ sealed class VoiceMessageState {
|
||||||
val isSending: Boolean,
|
val isSending: Boolean,
|
||||||
val isPlaying: Boolean,
|
val isPlaying: Boolean,
|
||||||
val playbackProgress: Float,
|
val playbackProgress: Float,
|
||||||
|
val time: Duration,
|
||||||
val waveform: ImmutableList<Float>,
|
val waveform: ImmutableList<Float>,
|
||||||
): VoiceMessageState()
|
): VoiceMessageState()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8521c3615973ebf8b5d09a596a6122866648f45fa2015b3261dcb64505ebc41d
|
oid sha256:1040e8767f3ce29d40842cb054dea64dc0d355b91ae86b1d25280b9308cdbda7
|
||||||
size 22459
|
size 24517
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:93a4628b19a5d8a5b1f064a0c5e6d8d90e2690481b10b34c1beca2cc84b3d34c
|
oid sha256:162a6b2a725ecfda754f8fe8bc06da0bd6b7efd7eb217a2e77b1ece57f89d5d6
|
||||||
size 20848
|
size 22936
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4008401117a064b6eed9cf06ada8c15273a52dd729ae0fe989d075808c928cb5
|
oid sha256:c692c6d70f847d37dce6c1cdbe630621cc6bdf37d48e24b4f95aa79d9422f15d
|
||||||
size 27376
|
size 28039
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b03f6a351e4f6a22ca78b1f7b703568cef9855920ae03089c300a60c0fc593fb
|
oid sha256:2f9b34504384622feed1348a00172d520123512c433973094634d159850a7653
|
||||||
size 26401
|
size 27124
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue