Merge pull request #4045 from element-hq/feature/bma/inlinePlayer
Inline voice message player in the files gallery.
This commit is contained in:
@@ -125,3 +125,24 @@ fun anAudioMediaInfo(
|
||||
dateSentFull = dateSentFull,
|
||||
waveform = waveForm,
|
||||
)
|
||||
|
||||
fun aVoiceMediaInfo(
|
||||
filename: String = "a voice file.ogg",
|
||||
caption: String? = null,
|
||||
senderName: String? = null,
|
||||
dateSent: String? = null,
|
||||
dateSentFull: String? = null,
|
||||
waveForm: List<Float>? = null,
|
||||
): MediaInfo = MediaInfo(
|
||||
filename = filename,
|
||||
caption = caption,
|
||||
mimeType = MimeTypes.Ogg,
|
||||
formattedFileSize = "3MB",
|
||||
fileExtension = "ogg",
|
||||
senderId = UserId("@alice:server.org"),
|
||||
senderName = senderName,
|
||||
senderAvatar = null,
|
||||
dateSent = dateSent,
|
||||
dateSentFull = dateSentFull,
|
||||
waveform = waveForm,
|
||||
)
|
||||
|
||||
@@ -43,6 +43,7 @@ dependencies {
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.voiceplayer.api)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
api(projects.libraries.mediaviewer.api)
|
||||
|
||||
@@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
|
||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractor
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -104,7 +105,6 @@ class EventItemFactory @Inject constructor(
|
||||
),
|
||||
mediaSource = type.source,
|
||||
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
||||
waveform = null,
|
||||
)
|
||||
is FileMessageType -> MediaItem.File(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
@@ -182,7 +182,7 @@ class EventItemFactory @Inject constructor(
|
||||
thumbnailSource = type.info?.thumbnailSource,
|
||||
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
||||
)
|
||||
is VoiceMessageType -> MediaItem.Audio(
|
||||
is VoiceMessageType -> MediaItem.Voice(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
eventId = currentTimelineItem.eventId,
|
||||
mediaInfo = MediaInfo(
|
||||
@@ -200,7 +200,7 @@ class EventItemFactory @Inject constructor(
|
||||
),
|
||||
mediaSource = type.source,
|
||||
duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(),
|
||||
waveform = type.details?.waveform,
|
||||
waveform = type.details?.waveform ?: persistentListOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
@@ -18,12 +19,15 @@ import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.di.LocalMediaItemPresenterFactories
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresenterFactories
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class MediaGalleryNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
presenterFactory: MediaGalleryPresenter.Factory,
|
||||
private val mediaItemPresenterFactories: MediaItemPresenterFactories,
|
||||
) : Node(buildContext, plugins = plugins),
|
||||
MediaGalleryNavigator {
|
||||
private val presenter = presenterFactory.create(
|
||||
@@ -56,12 +60,16 @@ class MediaGalleryNode @AssistedInject constructor(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
MediaGalleryView(
|
||||
state = state,
|
||||
onBackClick = ::onBackClick,
|
||||
onItemClick = ::onItemClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
LocalMediaItemPresenterFactories provides mediaItemPresenterFactories,
|
||||
) {
|
||||
val state = presenter.present()
|
||||
MediaGalleryView(
|
||||
state = state,
|
||||
onBackClick = ::onBackClick,
|
||||
onItemClick = ::onItemClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ class MediaGalleryPresenter @AssistedInject constructor(
|
||||
is MediaItem.Video -> event.mediaItem.thumbnailSource ?: event.mediaItem.mediaSource
|
||||
is MediaItem.Audio -> null
|
||||
is MediaItem.File -> null
|
||||
is MediaItem.Voice -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVoice
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
open class MediaGalleryStateProvider : PreviewParameterProvider<MediaGalleryState> {
|
||||
@@ -63,9 +64,8 @@ open class MediaGalleryStateProvider : PreviewParameterProvider<MediaGalleryStat
|
||||
id = UniqueId("2"),
|
||||
formattedDate = "September 2004",
|
||||
),
|
||||
aMediaItemFile(id = UniqueId("3")),
|
||||
aMediaItemAudio(id = UniqueId("4")),
|
||||
aMediaItemAudio(
|
||||
aMediaItemVoice(
|
||||
id = UniqueId("5"),
|
||||
waveform = aWaveForm(),
|
||||
),
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
@@ -40,6 +41,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.PageTitle
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncFailure
|
||||
@@ -60,11 +62,16 @@ import io.element.android.libraries.mediaviewer.impl.R
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaDeleteConfirmationBottomSheet
|
||||
import io.element.android.libraries.mediaviewer.impl.details.MediaDetailsBottomSheet
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.di.LocalMediaItemPresenterFactories
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.di.aFakeMediaItemPresenterFactories
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.di.rememberPresenter
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.AudioItemView
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.DateItemView
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.FileItemView
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.ImageItemView
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.VideoItemView
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.VoiceItemView
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlin.math.max
|
||||
|
||||
@@ -255,6 +262,7 @@ private fun MediaGalleryFilesList(
|
||||
eventSink: (MediaGalleryEvents) -> Unit,
|
||||
onItemClick: (MediaItem.Event) -> Unit,
|
||||
) {
|
||||
val presenterFactories = LocalMediaItemPresenterFactories.current
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
@@ -274,6 +282,16 @@ private fun MediaGalleryFilesList(
|
||||
onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) },
|
||||
onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) },
|
||||
)
|
||||
is MediaItem.Voice -> {
|
||||
val presenter: Presenter<VoiceMessageState> = presenterFactories.rememberPresenter(item)
|
||||
VoiceItemView(
|
||||
presenter.present(),
|
||||
item,
|
||||
onShareClick = { eventSink(MediaGalleryEvents.Share(item)) },
|
||||
onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) },
|
||||
onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) },
|
||||
)
|
||||
}
|
||||
is MediaItem.DateSeparator -> DateItemView(item)
|
||||
is MediaItem.Image,
|
||||
is MediaItem.Video -> {
|
||||
@@ -332,6 +350,9 @@ private fun MediaGalleryImageGrid(
|
||||
is MediaItem.Audio -> {
|
||||
// Should not happen
|
||||
}
|
||||
is MediaItem.Voice -> {
|
||||
// Should not happen
|
||||
}
|
||||
is MediaItem.File -> {
|
||||
// Should not happen
|
||||
}
|
||||
@@ -452,9 +473,13 @@ private fun LoadingContent(
|
||||
internal fun MediaGalleryViewPreview(
|
||||
@PreviewParameter(MediaGalleryStateProvider::class) state: MediaGalleryState
|
||||
) = ElementPreview {
|
||||
MediaGalleryView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onItemClick = {},
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
LocalMediaItemPresenterFactories provides aFakeMediaItemPresenterFactories(),
|
||||
) {
|
||||
MediaGalleryView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
onItemClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,15 @@ sealed interface MediaItem {
|
||||
val mediaInfo: MediaInfo,
|
||||
val mediaSource: MediaSource,
|
||||
val duration: String?,
|
||||
val waveform: ImmutableList<Float>?,
|
||||
) : Event
|
||||
|
||||
data class Voice(
|
||||
val id: UniqueId,
|
||||
val eventId: EventId?,
|
||||
val mediaInfo: MediaInfo,
|
||||
val mediaSource: MediaSource,
|
||||
val duration: String?,
|
||||
val waveform: ImmutableList<Float>,
|
||||
) : Event
|
||||
|
||||
data class File(
|
||||
@@ -77,6 +85,7 @@ fun MediaItem.id(): UniqueId {
|
||||
is MediaItem.Video -> id
|
||||
is MediaItem.File -> id
|
||||
is MediaItem.Audio -> id
|
||||
is MediaItem.Voice -> id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +95,7 @@ fun MediaItem.Event.eventId(): EventId? {
|
||||
is MediaItem.Video -> eventId
|
||||
is MediaItem.File -> eventId
|
||||
is MediaItem.Audio -> eventId
|
||||
is MediaItem.Voice -> eventId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +105,7 @@ fun MediaItem.Event.mediaInfo(): MediaInfo {
|
||||
is MediaItem.Video -> mediaInfo
|
||||
is MediaItem.File -> mediaInfo
|
||||
is MediaItem.Audio -> mediaInfo
|
||||
is MediaItem.Voice -> mediaInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +115,7 @@ fun MediaItem.Event.mediaSource(): MediaSource {
|
||||
is MediaItem.Video -> mediaSource
|
||||
is MediaItem.File -> mediaSource
|
||||
is MediaItem.Audio -> mediaSource
|
||||
is MediaItem.Voice -> mediaSource
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,5 +125,6 @@ fun MediaItem.Event.thumbnailSource(): MediaSource? {
|
||||
is MediaItem.Video -> thumbnailSource
|
||||
is MediaItem.File -> null
|
||||
is MediaItem.Audio -> null
|
||||
is MediaItem.Voice -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class MediaItemsPostProcessor @Inject constructor() {
|
||||
imageAndVideoItemsSubList.add(0, item)
|
||||
}
|
||||
is MediaItem.Audio,
|
||||
is MediaItem.Voice,
|
||||
is MediaItem.File -> {
|
||||
fileItemsSublist.add(0, item)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery.di
|
||||
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||
import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
|
||||
|
||||
/**
|
||||
* A fake [MediaItemPresenterFactories] for screenshot tests.
|
||||
*/
|
||||
fun aFakeMediaItemPresenterFactories() = MediaItemPresenterFactories(
|
||||
mapOf(
|
||||
Pair(
|
||||
MediaItem.Voice::class.java,
|
||||
MediaItemPresenterFactory<MediaItem.Voice, VoiceMessageState> { Presenter { aVoiceMessageState() } },
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery.di
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
|
||||
/**
|
||||
* Provides a [MediaItemPresenterFactories] to the composition.
|
||||
*/
|
||||
val LocalMediaItemPresenterFactories = staticCompositionLocalOf {
|
||||
MediaItemPresenterFactories(emptyMap())
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery.di
|
||||
|
||||
import dagger.MapKey
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Annotation to add a factory of type [MediaItemPresenterFactory] to a
|
||||
* Dagger map multi binding keyed with a subclass of [MediaItem.Event].
|
||||
*/
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MapKey
|
||||
annotation class MediaItemEventContentKey(val value: KClass<out MediaItem.Event>)
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery.di
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.multibindings.Multibinds
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Dagger module that declares the [MediaItemPresenterFactory] map multi binding.
|
||||
*
|
||||
* Its sole purpose is to support the case of an empty map multibinding.
|
||||
*/
|
||||
@Module
|
||||
@ContributesTo(RoomScope::class)
|
||||
interface MediaItemPresenterFactoriesModule {
|
||||
@Multibinds
|
||||
fun multiBindMediaItemPresenterFactories(): @JvmSuppressWildcards Map<Class<out MediaItem.Event>, MediaItemPresenterFactory<*, *>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Room level caching layer for the [MediaItemPresenterFactory] instances.
|
||||
*
|
||||
* It will cache the presenter instances in the room scope, so that they can be
|
||||
* reused across recompositions of the gallery items that happen whenever an item
|
||||
* goes out of the [LazyColumn] viewport.
|
||||
*/
|
||||
@SingleIn(RoomScope::class)
|
||||
class MediaItemPresenterFactories @Inject constructor(
|
||||
private val factories: @JvmSuppressWildcards Map<Class<out MediaItem.Event>, MediaItemPresenterFactory<*, *>>,
|
||||
) {
|
||||
private val presenters: MutableMap<MediaItem.Event, Presenter<*>> = mutableMapOf()
|
||||
|
||||
/**
|
||||
* Creates and caches a presenter for the given content.
|
||||
*
|
||||
* Will throw if the presenter is not found in the [MediaItemPresenterFactory] map multi binding.
|
||||
*
|
||||
* @param C The [MediaItem.Event] subtype handled by this TimelineItem presenter.
|
||||
* @param S The state type produced by this timeline item presenter.
|
||||
* @param content The [MediaItem.Event] instance to create a presenter for.
|
||||
* @param contentClass The class of [content].
|
||||
* @return An instance of a TimelineItem presenter that will be cached in the room scope.
|
||||
*/
|
||||
@Composable
|
||||
fun <C : MediaItem.Event, S : Any> rememberPresenter(
|
||||
content: C,
|
||||
contentClass: Class<C>,
|
||||
): Presenter<S> = remember(content) {
|
||||
presenters[content]?.let {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
it as Presenter<S>
|
||||
} ?: factories.getValue(contentClass).let {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(it as MediaItemPresenterFactory<C, S>).create(content).apply {
|
||||
presenters[content] = this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and caches a presenter for the given content.
|
||||
*
|
||||
* Will throw if the presenter is not found in the [MediaItemPresenterFactory] map multi binding.
|
||||
*
|
||||
* @param C The [MediaItem.Event] subtype handled by this TimelineItem presenter.
|
||||
* @param S The state type produced by this timeline item presenter.
|
||||
* @param content The [MediaItem.Event] instance to create a presenter for.
|
||||
* @return An instance of a TimelineItem presenter that will be cached in the room scope.
|
||||
*/
|
||||
@Composable
|
||||
inline fun <reified C : MediaItem.Event, S : Any> MediaItemPresenterFactories.rememberPresenter(
|
||||
content: C
|
||||
): Presenter<S> = rememberPresenter(
|
||||
content = content,
|
||||
contentClass = C::class.java
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery.di
|
||||
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||
|
||||
/**
|
||||
* A factory for a [Presenter] associated with a timeline item.
|
||||
*
|
||||
* Implementations should be annotated with [AssistedFactory] to be created by Dagger.
|
||||
*
|
||||
* @param C The timeline item's [MediaItem.Event] subtype.
|
||||
* @param S The [Presenter]'s state class.
|
||||
* @return A [Presenter] that produces a state of type [S] for the given content of type [C].
|
||||
*/
|
||||
fun interface MediaItemPresenterFactory<C : MediaItem.Event, S : Any> {
|
||||
fun create(content: C): Presenter<S>
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -21,6 +20,8 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.GraphicEq
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -31,7 +32,6 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.core.extensions.withBrackets
|
||||
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
@@ -39,7 +39,6 @@ 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.mediaviewer.impl.gallery.MediaItem
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Composable
|
||||
fun AudioItemView(
|
||||
@@ -94,18 +93,12 @@ private fun FilenameRow(
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = ElementTheme.colors.bgCanvasDefault,
|
||||
color = ElementTheme.colors.bgActionSecondaryRest,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = ElementTheme.colors.borderInteractiveSecondary,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.size(36.dp)
|
||||
.size(32.dp)
|
||||
.padding(6.dp),
|
||||
imageVector = CompoundIcons.PlaySolid(),
|
||||
tint = ElementTheme.colors.iconSecondary,
|
||||
imageVector = Icons.Outlined.GraphicEq,
|
||||
contentDescription = null,
|
||||
)
|
||||
audio.duration?.let {
|
||||
@@ -119,34 +112,20 @@ private fun FilenameRow(
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
val waveform = audio.waveform
|
||||
if (waveform == null) {
|
||||
Text(
|
||||
text = audio.mediaInfo.filename,
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
val formattedSize = audio.mediaInfo.formattedFileSize
|
||||
if (formattedSize.isNotEmpty()) {
|
||||
Text(
|
||||
text = audio.mediaInfo.filename,
|
||||
modifier = Modifier.weight(1f),
|
||||
text = formattedSize.withBrackets(),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
val formattedSize = audio.mediaInfo.formattedFileSize
|
||||
if (formattedSize.isNotEmpty()) {
|
||||
Text(
|
||||
text = formattedSize.withBrackets(),
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
WaveformPlaybackView(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(34.dp),
|
||||
playbackProgress = 0f,
|
||||
showCursor = false,
|
||||
waveform = waveform.toPersistentList(),
|
||||
onSeek = {},
|
||||
seekEnabled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ fun FileItemView(
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp, start = 16.dp, end = 16.dp),
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp, start = 16.dp, end = 16.dp),
|
||||
) {
|
||||
FilenameRow(
|
||||
file = file,
|
||||
@@ -78,24 +78,24 @@ private fun FilenameRow(
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
color = ElementTheme.colors.bgSubtleSecondary,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
.clickable { onClick() }
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
color = ElementTheme.colors.bgSubtleSecondary,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
.clickable { onClick() }
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = ElementTheme.colors.bgActionSecondaryRest,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.size(32.dp)
|
||||
.padding(6.dp),
|
||||
.background(
|
||||
color = ElementTheme.colors.bgActionSecondaryRest,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.size(32.dp)
|
||||
.padding(6.dp),
|
||||
imageVector = CompoundIcons.Attachment(),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
@@ -9,12 +9,10 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.core.preview.loremIpsum
|
||||
import io.element.android.libraries.designsystem.components.media.aWaveForm
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class MediaItemAudioProvider : PreviewParameterProvider<MediaItem.Audio> {
|
||||
override val values: Sequence<MediaItem.Audio>
|
||||
@@ -27,9 +25,6 @@ class MediaItemAudioProvider : PreviewParameterProvider<MediaItem.Audio> {
|
||||
aMediaItemAudio(
|
||||
caption = loremIpsum,
|
||||
),
|
||||
aMediaItemAudio(
|
||||
waveform = aWaveForm(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,7 +33,6 @@ fun aMediaItemAudio(
|
||||
filename: String = "filename",
|
||||
caption: String? = null,
|
||||
duration: String? = "1:23",
|
||||
waveform: List<Float>? = null,
|
||||
): MediaItem.Audio {
|
||||
return MediaItem.Audio(
|
||||
id = id,
|
||||
@@ -49,6 +43,5 @@ fun aMediaItemAudio(
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
duration = duration,
|
||||
waveform = waveform?.toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.core.preview.loremIpsum
|
||||
import io.element.android.libraries.designsystem.components.media.aWaveForm
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.mediaviewer.api.aVoiceMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class MediaItemVoiceProvider : PreviewParameterProvider<MediaItem.Voice> {
|
||||
override val values: Sequence<MediaItem.Voice>
|
||||
get() = sequenceOf(
|
||||
aMediaItemVoice(),
|
||||
aMediaItemVoice(
|
||||
filename = "A long filename that should be truncated.ogg",
|
||||
caption = "A caption",
|
||||
),
|
||||
aMediaItemVoice(
|
||||
caption = loremIpsum,
|
||||
),
|
||||
aMediaItemVoice(
|
||||
waveform = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aMediaItemVoice(
|
||||
id: UniqueId = UniqueId("fileId"),
|
||||
filename: String = "filename.ogg",
|
||||
caption: String? = null,
|
||||
duration: String? = "1:23",
|
||||
waveform: List<Float> = aWaveForm(),
|
||||
): MediaItem.Voice {
|
||||
return MediaItem.Voice(
|
||||
id = id,
|
||||
eventId = null,
|
||||
mediaInfo = aVoiceMediaInfo(
|
||||
filename = filename,
|
||||
caption = caption,
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
duration = duration,
|
||||
waveform = waveform.toImmutableList(),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
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.mediaviewer.impl.gallery.MediaItem
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider
|
||||
import io.element.android.libraries.voiceplayer.api.aVoiceMessageState
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun VoiceItemView(
|
||||
state: VoiceMessageState,
|
||||
voice: MediaItem.Voice,
|
||||
onShareClick: () -> Unit,
|
||||
onDownloadClick: () -> Unit,
|
||||
onInfoClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp, start = 16.dp, end = 16.dp),
|
||||
) {
|
||||
VoiceInfoRow(
|
||||
state = state,
|
||||
voice = voice,
|
||||
)
|
||||
val caption = voice.mediaInfo.caption
|
||||
if (caption != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Caption(caption)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ActionIconsRow(
|
||||
onShareClick = onShareClick,
|
||||
onDownloadClick = onDownloadClick,
|
||||
onInfoClick = onInfoClick,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VoiceInfoRow(
|
||||
state: VoiceMessageState,
|
||||
voice: MediaItem.Voice,
|
||||
) {
|
||||
fun playPause() {
|
||||
state.eventSink(VoiceMessageEvents.PlayPause)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
color = ElementTheme.colors.bgSubtleSecondary,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
when (state.button) {
|
||||
VoiceMessageState.Button.Play -> PlayButton(onClick = ::playPause)
|
||||
VoiceMessageState.Button.Pause -> PauseButton(onClick = ::playPause)
|
||||
VoiceMessageState.Button.Downloading -> ProgressButton()
|
||||
VoiceMessageState.Button.Retry -> RetryButton(onClick = ::playPause)
|
||||
VoiceMessageState.Button.Disabled -> PlayButton(onClick = {}, enabled = false)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = state.time,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
WaveformPlaybackView(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(34.dp),
|
||||
showCursor = state.showCursor,
|
||||
playbackProgress = state.progress,
|
||||
waveform = voice.waveform.toPersistentList(),
|
||||
onSeek = {
|
||||
state.eventSink(VoiceMessageEvents.Seek(it))
|
||||
},
|
||||
seekEnabled = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress button is shown when the voice message is being downloaded.
|
||||
*
|
||||
* The progress indicator is optimistic and displays a pause button (which
|
||||
* indicates the audio is playing) for 2 seconds before revealing the
|
||||
* actual progress indicator.
|
||||
*/
|
||||
@Composable
|
||||
private fun ProgressButton(
|
||||
displayImmediately: Boolean = false,
|
||||
) {
|
||||
var canDisplay by remember { mutableStateOf(displayImmediately) }
|
||||
LaunchedEffect(Unit) {
|
||||
delay(2000L)
|
||||
canDisplay = true
|
||||
}
|
||||
CustomIconButton(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
) {
|
||||
if (canDisplay) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(2.dp)
|
||||
.size(16.dp),
|
||||
color = ElementTheme.colors.iconSecondary,
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
} else {
|
||||
ControlIcon(
|
||||
imageVector = CompoundIcons.PauseSolid(),
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_pause),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlayButton(
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
CustomIconButton(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
) {
|
||||
ControlIcon(
|
||||
imageVector = CompoundIcons.PlaySolid(),
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_play),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PauseButton(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
CustomIconButton(
|
||||
onClick = onClick,
|
||||
) {
|
||||
ControlIcon(
|
||||
imageVector = CompoundIcons.PauseSolid(),
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_pause),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RetryButton(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
CustomIconButton(
|
||||
onClick = onClick,
|
||||
) {
|
||||
ControlIcon(
|
||||
imageVector = CompoundIcons.Restart(),
|
||||
contentDescription = stringResource(id = CommonStrings.action_retry),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ControlIcon(
|
||||
imageVector: ImageVector,
|
||||
contentDescription: String?,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(vertical = 10.dp),
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomIconButton(
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.background(color = ElementTheme.colors.bgCanvasDefault, shape = CircleShape)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = ElementTheme.colors.borderInteractiveSecondary,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.size(36.dp),
|
||||
enabled = enabled,
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = ElementTheme.colors.iconSecondary,
|
||||
disabledContentColor = ElementTheme.colors.iconDisabled,
|
||||
),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Caption(caption: String) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = caption,
|
||||
maxLines = 5,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionIconsRow(
|
||||
onShareClick: () -> Unit,
|
||||
onDownloadClick: () -> Unit,
|
||||
onInfoClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onShareClick,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.ShareAndroid(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onDownloadClick,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Download(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = onInfoClick,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Info(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun VoiceItemViewPreview(
|
||||
@PreviewParameter(MediaItemVoiceProvider::class) voice: MediaItem.Voice,
|
||||
) = ElementPreview {
|
||||
VoiceItemView(
|
||||
state = aVoiceMessageState(),
|
||||
voice = voice,
|
||||
onShareClick = {},
|
||||
onDownloadClick = {},
|
||||
onInfoClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun VoiceItemViewPlayPreview(
|
||||
@PreviewParameter(VoiceMessageStateProvider::class) state: VoiceMessageState,
|
||||
) = ElementPreview {
|
||||
VoiceItemView(
|
||||
state = state,
|
||||
voice = aMediaItemVoice(),
|
||||
onShareClick = {},
|
||||
onDownloadClick = {},
|
||||
onInfoClick = {},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaviewer.impl.gallery.voice
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemEventContentKey
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresenterFactory
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory
|
||||
import io.element.android.libraries.voiceplayer.api.VoiceMessageState
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Module
|
||||
@ContributesTo(RoomScope::class)
|
||||
interface VoiceMessagePresenterModule {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@MediaItemEventContentKey(MediaItem.Voice::class)
|
||||
fun bindVoiceMessagePresenterFactory(factory: VoiceMessagePresenter.Factory): MediaItemPresenterFactory<*, *>
|
||||
}
|
||||
|
||||
class VoiceMessagePresenter @AssistedInject constructor(
|
||||
voiceMessagePresenterFactory: VoiceMessagePresenterFactory,
|
||||
@Assisted private val item: MediaItem.Voice,
|
||||
) : Presenter<VoiceMessageState> {
|
||||
@AssistedFactory
|
||||
fun interface Factory : MediaItemPresenterFactory<MediaItem.Voice, VoiceMessageState> {
|
||||
override fun create(content: MediaItem.Voice): VoiceMessagePresenter
|
||||
}
|
||||
|
||||
private val presenter = voiceMessagePresenterFactory.createVoiceMessagePresenter(
|
||||
eventId = item.eventId,
|
||||
mediaSource = item.mediaSource,
|
||||
mimeType = item.mediaInfo.mimeType,
|
||||
filename = item.mediaInfo.filename,
|
||||
// TODO Get the duration for the fallback?
|
||||
duration = Duration.ZERO,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun present(): VoiceMessageState {
|
||||
return presenter.present()
|
||||
}
|
||||
}
|
||||
@@ -263,7 +263,6 @@ class DefaultEventItemFactoryTest {
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
duration = "7:36",
|
||||
waveform = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -348,7 +347,7 @@ class DefaultEventItemFactoryTest {
|
||||
)
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
MediaItem.Audio(
|
||||
MediaItem.Voice(
|
||||
id = A_UNIQUE_ID,
|
||||
eventId = AN_EVENT_ID,
|
||||
mediaInfo = MediaInfo(
|
||||
|
||||
@@ -17,6 +17,7 @@ import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemFile
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemLoadingIndicator
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVideo
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemVoice
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.Test
|
||||
|
||||
@@ -27,6 +28,9 @@ class MediaItemsPostProcessorTest {
|
||||
private val audio1 = aMediaItemAudio(id = UniqueId("1"))
|
||||
private val audio2 = aMediaItemAudio(id = UniqueId("2"))
|
||||
private val audio3 = aMediaItemAudio(id = UniqueId("3"))
|
||||
private val voice1 = aMediaItemVoice(id = UniqueId("1"))
|
||||
private val voice2 = aMediaItemVoice(id = UniqueId("2"))
|
||||
private val voice3 = aMediaItemVoice(id = UniqueId("3"))
|
||||
private val image1 = aMediaItemImage(id = UniqueId("1"))
|
||||
private val image2 = aMediaItemImage(id = UniqueId("2"))
|
||||
private val image3 = aMediaItemImage(id = UniqueId("3"))
|
||||
@@ -163,6 +167,9 @@ class MediaItemsPostProcessorTest {
|
||||
fun `process will handle complex case`() {
|
||||
test(
|
||||
mediaItems = listOf(
|
||||
voice3,
|
||||
voice2,
|
||||
voice1,
|
||||
audio3,
|
||||
audio2,
|
||||
audio1,
|
||||
@@ -192,6 +199,9 @@ class MediaItemsPostProcessorTest {
|
||||
audio1,
|
||||
audio2,
|
||||
audio3,
|
||||
voice1,
|
||||
voice2,
|
||||
voice3,
|
||||
date3,
|
||||
file3,
|
||||
loading1,
|
||||
|
||||
@@ -128,6 +128,7 @@ class KonsistPreviewTest {
|
||||
"TimelineVideoWithCaptionRowPreview",
|
||||
"TimelineViewMessageShieldPreview",
|
||||
"UserAvatarColorsPreview",
|
||||
"VoiceItemViewPlayPreview",
|
||||
)
|
||||
.assertTrue(
|
||||
additionalMessage = "Functions for Preview should be named like this: <ViewUnderPreview>Preview. " +
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -224,6 +224,7 @@ Compose:
|
||||
- LocalCompoundColors
|
||||
- LocalSnackbarDispatcher
|
||||
- LocalCameraPositionState
|
||||
- LocalMediaItemPresenterFactories
|
||||
- LocalTimelineItemPresenterFactories
|
||||
- LocalRoomMemberProfilesCache
|
||||
- LocalMentionSpanTheme
|
||||
|
||||
Reference in New Issue
Block a user