Skip to content

Commit 75e8ec6

Browse files
committed
Add inline voice player to the files gallery
1 parent 49b413b commit 75e8ec6

File tree

12 files changed

+449
-48
lines changed

12 files changed

+449
-48
lines changed

libraries/mediaviewer/impl/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies {
4343
implementation(projects.libraries.matrix.api)
4444
implementation(projects.libraries.matrixui)
4545
implementation(projects.libraries.uiStrings)
46+
implementation(projects.libraries.voiceplayer.api)
4647
implementation(projects.services.toolbox.api)
4748

4849
api(projects.libraries.mediaviewer.api)

libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package io.element.android.libraries.mediaviewer.impl.gallery
99

1010
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.CompositionLocalProvider
1112
import androidx.compose.ui.Modifier
1213
import com.bumble.appyx.core.modality.BuildContext
1314
import com.bumble.appyx.core.node.Node
@@ -18,12 +19,15 @@ import dagger.assisted.AssistedInject
1819
import io.element.android.anvilannotations.ContributesNode
1920
import io.element.android.libraries.di.RoomScope
2021
import io.element.android.libraries.matrix.api.core.EventId
22+
import io.element.android.libraries.mediaviewer.impl.gallery.di.LocalMediaItemPresenterFactories
23+
import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresenterFactories
2124

2225
@ContributesNode(RoomScope::class)
2326
class MediaGalleryNode @AssistedInject constructor(
2427
@Assisted buildContext: BuildContext,
2528
@Assisted plugins: List<Plugin>,
2629
presenterFactory: MediaGalleryPresenter.Factory,
30+
private val mediaItemPresenterFactories: MediaItemPresenterFactories,
2731
) : Node(buildContext, plugins = plugins),
2832
MediaGalleryNavigator {
2933
private val presenter = presenterFactory.create(
@@ -56,12 +60,16 @@ class MediaGalleryNode @AssistedInject constructor(
5660

5761
@Composable
5862
override fun View(modifier: Modifier) {
59-
val state = presenter.present()
60-
MediaGalleryView(
61-
state = state,
62-
onBackClick = ::onBackClick,
63-
onItemClick = ::onItemClick,
64-
modifier = modifier,
65-
)
63+
CompositionLocalProvider(
64+
LocalMediaItemPresenterFactories provides mediaItemPresenterFactories,
65+
) {
66+
val state = presenter.present()
67+
MediaGalleryView(
68+
state = state,
69+
onBackClick = ::onBackClick,
70+
onItemClick = ::onItemClick,
71+
modifier = modifier,
72+
)
73+
}
6674
}
6775
}

libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ open class MediaGalleryStateProvider : PreviewParameterProvider<MediaGalleryStat
6464
id = UniqueId("2"),
6565
formattedDate = "September 2004",
6666
),
67-
aMediaItemFile(id = UniqueId("3")),
6867
aMediaItemAudio(id = UniqueId("4")),
6968
aMediaItemVoice(
7069
id = UniqueId("5"),

libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import androidx.compose.foundation.pager.rememberPagerState
2727
import androidx.compose.material3.ExperimentalMaterial3Api
2828
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
2929
import androidx.compose.runtime.Composable
30+
import androidx.compose.runtime.CompositionLocalProvider
3031
import androidx.compose.runtime.LaunchedEffect
3132
import androidx.compose.runtime.getValue
3233
import androidx.compose.runtime.rememberUpdatedState
@@ -40,6 +41,7 @@ import androidx.compose.ui.unit.dp
4041
import io.element.android.compound.theme.ElementTheme
4142
import io.element.android.compound.tokens.generated.CompoundIcons
4243
import io.element.android.libraries.architecture.AsyncData
44+
import io.element.android.libraries.architecture.Presenter
4345
import io.element.android.libraries.designsystem.components.BigIcon
4446
import io.element.android.libraries.designsystem.components.PageTitle
4547
import io.element.android.libraries.designsystem.components.async.AsyncFailure
@@ -60,12 +62,16 @@ import io.element.android.libraries.mediaviewer.impl.R
6062
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
6163
import io.element.android.libraries.mediaviewer.impl.details.MediaDeleteConfirmationBottomSheet
6264
import io.element.android.libraries.mediaviewer.impl.details.MediaDetailsBottomSheet
65+
import io.element.android.libraries.mediaviewer.impl.gallery.di.LocalMediaItemPresenterFactories
66+
import io.element.android.libraries.mediaviewer.impl.gallery.di.aFakeMediaItemPresenterFactories
67+
import io.element.android.libraries.mediaviewer.impl.gallery.di.rememberPresenter
6368
import io.element.android.libraries.mediaviewer.impl.gallery.ui.AudioItemView
6469
import io.element.android.libraries.mediaviewer.impl.gallery.ui.DateItemView
6570
import io.element.android.libraries.mediaviewer.impl.gallery.ui.FileItemView
6671
import io.element.android.libraries.mediaviewer.impl.gallery.ui.ImageItemView
6772
import io.element.android.libraries.mediaviewer.impl.gallery.ui.VideoItemView
6873
import io.element.android.libraries.mediaviewer.impl.gallery.ui.VoiceItemView
74+
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
6975
import kotlinx.collections.immutable.ImmutableList
7076
import kotlin.math.max
7177

@@ -256,6 +262,7 @@ private fun MediaGalleryFilesList(
256262
eventSink: (MediaGalleryEvents) -> Unit,
257263
onItemClick: (MediaItem.Event) -> Unit,
258264
) {
265+
val presenterFactories = LocalMediaItemPresenterFactories.current
259266
LazyColumn(
260267
modifier = Modifier.fillMaxSize(),
261268
) {
@@ -275,12 +282,16 @@ private fun MediaGalleryFilesList(
275282
onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) },
276283
onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) },
277284
)
278-
is MediaItem.Voice -> VoiceItemView(
279-
item,
280-
onShareClick = { eventSink(MediaGalleryEvents.Share(item)) },
281-
onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) },
282-
onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) },
283-
)
285+
is MediaItem.Voice -> {
286+
val presenter: Presenter<VoiceMessageState> = presenterFactories.rememberPresenter(item)
287+
VoiceItemView(
288+
presenter.present(),
289+
item,
290+
onShareClick = { eventSink(MediaGalleryEvents.Share(item)) },
291+
onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) },
292+
onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) },
293+
)
294+
}
284295
is MediaItem.DateSeparator -> DateItemView(item)
285296
is MediaItem.Image,
286297
is MediaItem.Video -> {
@@ -462,9 +473,13 @@ private fun LoadingContent(
462473
internal fun MediaGalleryViewPreview(
463474
@PreviewParameter(MediaGalleryStateProvider::class) state: MediaGalleryState
464475
) = ElementPreview {
465-
MediaGalleryView(
466-
state = state,
467-
onBackClick = {},
468-
onItemClick = {},
469-
)
476+
CompositionLocalProvider(
477+
LocalMediaItemPresenterFactories provides aFakeMediaItemPresenterFactories(),
478+
) {
479+
MediaGalleryView(
480+
state = state,
481+
onBackClick = {},
482+
onItemClick = {},
483+
)
484+
}
470485
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.mediaviewer.impl.gallery.di
9+
10+
import io.element.android.libraries.architecture.Presenter
11+
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
12+
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
13+
import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
14+
15+
/**
16+
* A fake [MediaItemPresenterFactories] for screenshot tests.
17+
*/
18+
fun aFakeMediaItemPresenterFactories() = MediaItemPresenterFactories(
19+
mapOf(
20+
Pair(
21+
MediaItem.Voice::class.java,
22+
MediaItemPresenterFactory<MediaItem.Voice, VoiceMessageState> { Presenter { aVoiceMessageState() } },
23+
),
24+
)
25+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.mediaviewer.impl.gallery.di
9+
10+
import androidx.compose.runtime.staticCompositionLocalOf
11+
12+
/**
13+
* Provides a [MediaItemPresenterFactories] to the composition.
14+
*/
15+
val LocalMediaItemPresenterFactories = staticCompositionLocalOf {
16+
MediaItemPresenterFactories(emptyMap())
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.mediaviewer.impl.gallery.di
9+
10+
import dagger.MapKey
11+
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
12+
import kotlin.reflect.KClass
13+
14+
/**
15+
* Annotation to add a factory of type [MediaItemPresenterFactory] to a
16+
* Dagger map multi binding keyed with a subclass of [MediaItem.Event].
17+
*/
18+
@Retention(AnnotationRetention.RUNTIME)
19+
@MapKey
20+
annotation class MediaItemEventContentKey(val value: KClass<out MediaItem.Event>)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.mediaviewer.impl.gallery.di
9+
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.remember
12+
import com.squareup.anvil.annotations.ContributesTo
13+
import dagger.Module
14+
import dagger.multibindings.Multibinds
15+
import io.element.android.libraries.architecture.Presenter
16+
import io.element.android.libraries.di.RoomScope
17+
import io.element.android.libraries.di.SingleIn
18+
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
19+
import javax.inject.Inject
20+
21+
/**
22+
* Dagger module that declares the [MediaItemPresenterFactory] map multi binding.
23+
*
24+
* Its sole purpose is to support the case of an empty map multibinding.
25+
*/
26+
@Module
27+
@ContributesTo(RoomScope::class)
28+
interface MediaItemPresenterFactoriesModule {
29+
@Multibinds
30+
fun multiBindMediaItemPresenterFactories(): @JvmSuppressWildcards Map<Class<out MediaItem.Event>, MediaItemPresenterFactory<*, *>>
31+
}
32+
33+
/**
34+
* Room level caching layer for the [MediaItemPresenterFactory] instances.
35+
*
36+
* It will cache the presenter instances in the room scope, so that they can be
37+
* reused across recompositions of the gallery items that happen whenever an item
38+
* goes out of the [LazyColumn] viewport.
39+
*/
40+
@SingleIn(RoomScope::class)
41+
class MediaItemPresenterFactories @Inject constructor(
42+
private val factories: @JvmSuppressWildcards Map<Class<out MediaItem.Event>, MediaItemPresenterFactory<*, *>>,
43+
) {
44+
private val presenters: MutableMap<MediaItem.Event, Presenter<*>> = mutableMapOf()
45+
46+
/**
47+
* Creates and caches a presenter for the given content.
48+
*
49+
* Will throw if the presenter is not found in the [MediaItemPresenterFactory] map multi binding.
50+
*
51+
* @param C The [MediaItem.Event] subtype handled by this TimelineItem presenter.
52+
* @param S The state type produced by this timeline item presenter.
53+
* @param content The [MediaItem.Event] instance to create a presenter for.
54+
* @param contentClass The class of [content].
55+
* @return An instance of a TimelineItem presenter that will be cached in the room scope.
56+
*/
57+
@Composable
58+
fun <C : MediaItem.Event, S : Any> rememberPresenter(
59+
content: C,
60+
contentClass: Class<C>,
61+
): Presenter<S> = remember(content) {
62+
presenters[content]?.let {
63+
@Suppress("UNCHECKED_CAST")
64+
it as Presenter<S>
65+
} ?: factories.getValue(contentClass).let {
66+
@Suppress("UNCHECKED_CAST")
67+
(it as MediaItemPresenterFactory<C, S>).create(content).apply {
68+
presenters[content] = this
69+
}
70+
}
71+
}
72+
}
73+
74+
/**
75+
* Creates and caches a presenter for the given content.
76+
*
77+
* Will throw if the presenter is not found in the [MediaItemPresenterFactory] map multi binding.
78+
*
79+
* @param C The [MediaItem.Event] subtype handled by this TimelineItem presenter.
80+
* @param S The state type produced by this timeline item presenter.
81+
* @param content The [MediaItem.Event] instance to create a presenter for.
82+
* @return An instance of a TimelineItem presenter that will be cached in the room scope.
83+
*/
84+
@Composable
85+
inline fun <reified C : MediaItem.Event, S : Any> MediaItemPresenterFactories.rememberPresenter(
86+
content: C
87+
): Presenter<S> = rememberPresenter(
88+
content = content,
89+
contentClass = C::class.java
90+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.libraries.mediaviewer.impl.gallery.di
9+
10+
import io.element.android.libraries.architecture.Presenter
11+
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
12+
13+
/**
14+
* A factory for a [Presenter] associated with a timeline item.
15+
*
16+
* Implementations should be annotated with [AssistedFactory] to be created by Dagger.
17+
*
18+
* @param C The timeline item's [MediaItem.Event] subtype.
19+
* @param S The [Presenter]'s state class.
20+
* @return A [Presenter] that produces a state of type [S] for the given content of type [C].
21+
*/
22+
fun interface MediaItemPresenterFactory<C : MediaItem.Event, S : Any> {
23+
fun create(content: C): Presenter<S>
24+
}

0 commit comments

Comments
 (0)