Skip to content

Commit f74258d

Browse files
committed
Extract voice message player to its own module
1 parent b2a9ebb commit f74258d

File tree

23 files changed

+321
-141
lines changed

23 files changed

+321
-141
lines changed

features/messages/impl/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies {
4747
implementation(projects.libraries.permissions.api)
4848
implementation(projects.libraries.preferences.api)
4949
implementation(projects.libraries.roomselect.api)
50+
implementation(projects.libraries.voiceplayer.api)
5051
implementation(projects.libraries.voicerecorder.api)
5152
implementation(projects.libraries.mediaplayer.api)
5253
implementation(projects.libraries.uiUtils)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
2929
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
3030
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
3131
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
32-
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
3332
import io.element.android.libraries.architecture.Presenter
33+
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
3434

3535
@Composable
3636
fun TimelineItemEventContentView(

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ import io.element.android.compound.tokens.generated.CompoundIcons
4040
import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData
4141
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
4242
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentProvider
43-
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageEvents
44-
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
45-
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageStateProvider
4643
import io.element.android.libraries.androidutils.accessibility.isScreenReaderEnabled
4744
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
4845
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -52,6 +49,9 @@ import io.element.android.libraries.designsystem.theme.components.Icon
5249
import io.element.android.libraries.designsystem.theme.components.IconButton
5350
import io.element.android.libraries.designsystem.theme.components.Text
5451
import io.element.android.libraries.ui.strings.CommonStrings
52+
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
53+
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
54+
import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider
5555
import kotlinx.coroutines.delay
5656

5757
@Composable

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/FakeTimelineItemPresenterFactories.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
package io.element.android.features.messages.impl.timeline.di
99

1010
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
11-
import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessageState
12-
import io.element.android.features.messages.impl.voicemessages.timeline.aVoiceMessageState
1311
import io.element.android.libraries.architecture.Presenter
12+
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
13+
import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
1414

1515
/**
1616
* A fake [TimelineItemPresenterFactories] for screenshot tests.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ import androidx.core.net.toUri
2121
import androidx.lifecycle.Lifecycle
2222
import im.vector.app.features.analytics.plan.Composer
2323
import io.element.android.features.messages.api.MessageComposerContext
24-
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
2524
import io.element.android.libraries.architecture.Presenter
2625
import io.element.android.libraries.mediaupload.api.MediaSender
2726
import io.element.android.libraries.permissions.api.PermissionsEvents
2827
import io.element.android.libraries.permissions.api.PermissionsPresenter
2928
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
3029
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
3130
import io.element.android.libraries.textcomposer.model.VoiceMessageState
31+
import io.element.android.libraries.voiceplayer.api.VoiceMessageException
3232
import io.element.android.libraries.voicerecorder.api.VoiceRecorder
3333
import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
3434
import io.element.android.services.analytics.api.AnalyticsService

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt

Lines changed: 6 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@
88
package io.element.android.features.messages.impl.voicemessages.timeline
99

1010
import androidx.compose.runtime.Composable
11-
import androidx.compose.runtime.collectAsState
12-
import androidx.compose.runtime.derivedStateOf
13-
import androidx.compose.runtime.getValue
14-
import androidx.compose.runtime.mutableStateOf
15-
import androidx.compose.runtime.remember
1611
import com.squareup.anvil.annotations.ContributesTo
1712
import dagger.Binds
1813
import dagger.Module
@@ -23,17 +18,10 @@ import dagger.multibindings.IntoMap
2318
import io.element.android.features.messages.impl.timeline.di.TimelineItemEventContentKey
2419
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactory
2520
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
26-
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
27-
import io.element.android.libraries.architecture.AsyncData
2821
import io.element.android.libraries.architecture.Presenter
29-
import io.element.android.libraries.architecture.runUpdatingState
30-
import io.element.android.libraries.core.extensions.flatMap
3122
import io.element.android.libraries.di.RoomScope
32-
import io.element.android.libraries.ui.utils.time.formatShort
33-
import io.element.android.services.analytics.api.AnalyticsService
34-
import kotlinx.coroutines.CoroutineScope
35-
import kotlinx.coroutines.launch
36-
import kotlin.time.Duration.Companion.milliseconds
23+
import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory
24+
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
3725

3826
@Module
3927
@ContributesTo(RoomScope::class)
@@ -45,107 +33,24 @@ interface VoiceMessagePresenterModule {
4533
}
4634

4735
class VoiceMessagePresenter @AssistedInject constructor(
48-
voiceMessagePlayerFactory: VoiceMessagePlayer.Factory,
49-
private val analyticsService: AnalyticsService,
50-
private val scope: CoroutineScope,
36+
voiceMessagePresenterFactory: VoiceMessagePresenterFactory,
5137
@Assisted private val content: TimelineItemVoiceContent,
5238
) : Presenter<VoiceMessageState> {
5339
@AssistedFactory
5440
fun interface Factory : TimelineItemPresenterFactory<TimelineItemVoiceContent, VoiceMessageState> {
5541
override fun create(content: TimelineItemVoiceContent): VoiceMessagePresenter
5642
}
5743

58-
private val player = voiceMessagePlayerFactory.create(
44+
private val presenter = voiceMessagePresenterFactory.createVoiceMessagePresenter(
5945
eventId = content.eventId,
6046
mediaSource = content.mediaSource,
6147
mimeType = content.mimeType,
6248
filename = content.filename,
49+
duration = content.duration,
6350
)
6451

65-
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)
66-
6752
@Composable
6853
override fun present(): VoiceMessageState {
69-
val playerState by player.state.collectAsState(
70-
VoiceMessagePlayer.State(
71-
isReady = false,
72-
isPlaying = false,
73-
isEnded = false,
74-
currentPosition = 0L,
75-
duration = null
76-
)
77-
)
78-
79-
val button by remember {
80-
derivedStateOf {
81-
when {
82-
content.eventId == null -> VoiceMessageState.Button.Disabled
83-
playerState.isPlaying -> VoiceMessageState.Button.Pause
84-
play.value is AsyncData.Loading -> VoiceMessageState.Button.Downloading
85-
play.value is AsyncData.Failure -> VoiceMessageState.Button.Retry
86-
else -> VoiceMessageState.Button.Play
87-
}
88-
}
89-
}
90-
val duration by remember {
91-
derivedStateOf { playerState.duration ?: content.duration.inWholeMilliseconds }
92-
}
93-
val progress by remember {
94-
derivedStateOf {
95-
playerState.currentPosition / duration.toFloat()
96-
}
97-
}
98-
val time by remember {
99-
derivedStateOf {
100-
when {
101-
playerState.isReady && !playerState.isEnded -> playerState.currentPosition
102-
playerState.currentPosition > 0 -> playerState.currentPosition
103-
else -> duration
104-
}.milliseconds.formatShort()
105-
}
106-
}
107-
val showCursor by remember {
108-
derivedStateOf {
109-
!play.value.isUninitialized() && !playerState.isEnded
110-
}
111-
}
112-
113-
fun eventSink(event: VoiceMessageEvents) {
114-
when (event) {
115-
is VoiceMessageEvents.PlayPause -> {
116-
if (playerState.isPlaying) {
117-
player.pause()
118-
} else if (playerState.isReady) {
119-
player.play()
120-
} else {
121-
scope.launch {
122-
play.runUpdatingState(
123-
errorTransform = {
124-
analyticsService.trackError(
125-
VoiceMessageException.PlayMessageError("Error while trying to play voice message", it)
126-
)
127-
it
128-
},
129-
) {
130-
player.prepare().flatMap {
131-
runCatching { player.play() }
132-
}
133-
}
134-
}
135-
}
136-
}
137-
is VoiceMessageEvents.Seek -> {
138-
player.seekTo((event.percentage * duration).toLong())
139-
}
140-
}
141-
}
142-
143-
return VoiceMessageState(
144-
button = button,
145-
progress = progress,
146-
time = time,
147-
showCursor = showCursor,
148-
eventSink = { eventSink(it) },
149-
)
54+
return presenter.present()
15055
}
15156
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import app.cash.turbine.test
1818
import com.google.common.truth.Truth.assertThat
1919
import im.vector.app.features.analytics.plan.Composer
2020
import io.element.android.features.messages.impl.messagecomposer.aReplyMode
21-
import io.element.android.features.messages.impl.voicemessages.VoiceMessageException
2221
import io.element.android.features.messages.test.FakeMessageComposerContext
2322
import io.element.android.libraries.matrix.api.core.ProgressCallback
2423
import io.element.android.libraries.matrix.api.media.AudioInfo
@@ -36,6 +35,7 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode
3635
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
3736
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
3837
import io.element.android.libraries.textcomposer.model.VoiceMessageState
38+
import io.element.android.libraries.voiceplayer.api.VoiceMessageException
3939
import io.element.android.libraries.voicerecorder.test.FakeVoiceRecorder
4040
import io.element.android.services.analytics.test.FakeAnalyticsService
4141
import io.element.android.tests.testutils.WarmUpRule
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import extension.setupAnvil
2+
3+
/*
4+
* Copyright 2024 New Vector Ltd.
5+
*
6+
* SPDX-License-Identifier: AGPL-3.0-only
7+
* Please see LICENSE in the repository root for full details.
8+
*/
9+
plugins {
10+
id("io.element.android-compose-library")
11+
}
12+
13+
android {
14+
namespace = "io.element.android.libraries.voiceplayer.api"
15+
}
16+
17+
setupAnvil()
18+
19+
dependencies {
20+
implementation(libs.androidx.annotationjvm)
21+
implementation(libs.coroutines.core)
22+
implementation(projects.libraries.matrix.api)
23+
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessageEvents.kt renamed to libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageEvents.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Please see LICENSE in the repository root for full details.
66
*/
77

8-
package io.element.android.features.messages.impl.voicemessages.timeline
8+
package io.element.android.libraries.voiceplayer.api
99

1010
sealed interface VoiceMessageEvents {
1111
data object PlayPause : VoiceMessageEvents

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/VoiceMessageException.kt renamed to libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageException.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@
55
* Please see LICENSE in the repository root for full details.
66
*/
77

8-
package io.element.android.features.messages.impl.voicemessages
8+
package io.element.android.libraries.voiceplayer.api
99

10-
internal sealed class VoiceMessageException : Exception() {
10+
sealed class VoiceMessageException : Exception() {
1111
data class FileException(
1212
override val message: String?,
1313
override val cause: Throwable? = null
1414
) : VoiceMessageException()
15+
1516
data class PermissionMissing(
1617
override val message: String?,
1718
override val cause: Throwable?
1819
) : VoiceMessageException()
20+
1921
data class PlayMessageError(
2022
override val message: String?,
2123
override val cause: Throwable?

0 commit comments

Comments
 (0)