From 83e474f118502faf43f0890ca647c2551468bd2d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Dec 2024 09:58:02 +0100 Subject: [PATCH 1/5] Format file --- .../impl/gallery/ui/FileItemView.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt index 99ff456296..307ed89bdc 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt @@ -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, ) From df72ab6a86b00d0528c0c65c91509ddd6a48f95d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Dec 2024 09:53:02 +0100 Subject: [PATCH 2/5] Distinguish Audio and Voice media items. --- .../libraries/mediaviewer/api/MediaInfo.kt | 21 ++ .../impl/gallery/EventItemFactory.kt | 6 +- .../impl/gallery/MediaGalleryPresenter.kt | 1 + .../impl/gallery/MediaGalleryStateProvider.kt | 3 +- .../impl/gallery/MediaGalleryView.kt | 10 + .../mediaviewer/impl/gallery/MediaItem.kt | 15 +- .../impl/gallery/MediaItemsPostProcessor.kt | 1 + .../impl/gallery/ui/AudioItemView.kt | 53 ++--- .../impl/gallery/ui/MediaItemAudioProvider.kt | 7 - .../impl/gallery/ui/MediaItemVoiceProvider.kt | 54 +++++ .../impl/gallery/ui/VoiceItemView.kt | 191 ++++++++++++++++++ .../gallery/DefaultEventItemFactoryTest.kt | 3 +- .../gallery/MediaItemsPostProcessorTest.kt | 10 + 13 files changed, 324 insertions(+), 51 deletions(-) create mode 100644 libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt create mode 100644 libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt index 8d72d049ed..7f2e823b1e 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt @@ -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? = 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, +) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt index ede4c491ea..fbd4f647bc 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt @@ -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(), ) } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt index 242d23c10d..905ba2c770 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt @@ -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 }, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt index d148121dbd..c1d1e1ca72 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt @@ -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 { @@ -65,7 +66,7 @@ open class MediaGalleryStateProvider : PreviewParameterProvider VoiceItemView( + 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 +339,9 @@ private fun MediaGalleryImageGrid( is MediaItem.Audio -> { // Should not happen } + is MediaItem.Voice -> { + // Should not happen + } is MediaItem.File -> { // Should not happen } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItem.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItem.kt index 0925b4937b..222f620ec5 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItem.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItem.kt @@ -58,7 +58,15 @@ sealed interface MediaItem { val mediaInfo: MediaInfo, val mediaSource: MediaSource, val duration: String?, - val waveform: ImmutableList?, + ) : Event + + data class Voice( + val id: UniqueId, + val eventId: EventId?, + val mediaInfo: MediaInfo, + val mediaSource: MediaSource, + val duration: String?, + val waveform: ImmutableList, ) : 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 } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessor.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessor.kt index 1ed9f0e42b..229f547c3b 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessor.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessor.kt @@ -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) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt index a15a12ba58..9a7d498119 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt @@ -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, ) } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt index 4bcdf60d3a..1194449aef 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt @@ -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 { override val values: Sequence @@ -27,9 +25,6 @@ class MediaItemAudioProvider : PreviewParameterProvider { aMediaItemAudio( caption = loremIpsum, ), - aMediaItemAudio( - waveform = aWaveForm(), - ), ) } @@ -38,7 +33,6 @@ fun aMediaItemAudio( filename: String = "filename", caption: String? = null, duration: String? = "1:23", - waveform: List? = null, ): MediaItem.Audio { return MediaItem.Audio( id = id, @@ -49,6 +43,5 @@ fun aMediaItemAudio( ), mediaSource = MediaSource(""), duration = duration, - waveform = waveform?.toImmutableList(), ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt new file mode 100644 index 0000000000..8056c094e8 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt @@ -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 { + override val values: Sequence + 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 = aWaveForm(), +): MediaItem.Voice { + return MediaItem.Voice( + id = id, + eventId = null, + mediaInfo = aVoiceMediaInfo( + filename = filename, + caption = caption, + ), + mediaSource = MediaSource(""), + duration = duration, + waveform = waveform.toImmutableList(), + ) +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt new file mode 100644 index 0000000000..137a94e5d2 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt @@ -0,0 +1,191 @@ +/* + * 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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.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 kotlinx.collections.immutable.toPersistentList + +@Composable +fun VoiceItemView( + 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( + 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( + voice: MediaItem.Voice, +) { + 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, + ) { + Icon( + modifier = Modifier + .background( + color = ElementTheme.colors.bgCanvasDefault, + shape = CircleShape, + ) + .border( + width = 1.dp, + color = ElementTheme.colors.borderInteractiveSecondary, + shape = CircleShape, + ) + .size(36.dp) + .padding(6.dp), + imageVector = CompoundIcons.PlaySolid(), + tint = ElementTheme.colors.iconSecondary, + contentDescription = null, + ) + voice.duration?.let { + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = voice.duration, + style = ElementTheme.typography.fontBodyMdMedium, + color = ElementTheme.colors.textSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Spacer(modifier = Modifier.width(8.dp)) + WaveformPlaybackView( + modifier = Modifier + .weight(1f) + .height(34.dp), + playbackProgress = 0f, + showCursor = false, + waveform = voice.waveform.toPersistentList(), + onSeek = { + + }, + seekEnabled = true, + ) + } +} + +@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( + voice = voice, + onShareClick = {}, + onDownloadClick = {}, + onInfoClick = {}, + ) +} diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt index 3478f8f867..d074581f7d 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt @@ -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( diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessorTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessorTest.kt index cf32409248..9621413da6 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessorTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaItemsPostProcessorTest.kt @@ -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, From 0ed4d34329af91fe76588f33de934b3d3c1bda6c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Dec 2024 10:12:16 +0100 Subject: [PATCH 3/5] Add inline voice player to the files gallery --- libraries/mediaviewer/impl/build.gradle.kts | 1 + .../impl/gallery/MediaGalleryNode.kt | 22 +- .../impl/gallery/MediaGalleryStateProvider.kt | 1 - .../impl/gallery/MediaGalleryView.kt | 37 +++- .../di/FakeTimelineItemPresenterFactories.kt | 25 +++ .../di/LocalMediaItemPresenterFactories.kt | 17 ++ .../gallery/di/MediaItemEventContentKey.kt | 20 ++ .../gallery/di/MediaItemPresenterFactories.kt | 90 ++++++++ .../gallery/di/MediaItemPresenterFactory.kt | 24 +++ .../impl/gallery/ui/VoiceItemView.kt | 201 +++++++++++++++--- .../gallery/voice/VoiceMessagePresenter.kt | 58 +++++ .../tests/konsist/KonsistPreviewTest.kt | 1 + 12 files changed, 449 insertions(+), 48 deletions(-) create mode 100644 libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt create mode 100644 libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/LocalMediaItemPresenterFactories.kt create mode 100644 libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt create mode 100644 libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt create mode 100644 libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt create mode 100644 libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt diff --git a/libraries/mediaviewer/impl/build.gradle.kts b/libraries/mediaviewer/impl/build.gradle.kts index 4fa63820d3..395b57df38 100644 --- a/libraries/mediaviewer/impl/build.gradle.kts +++ b/libraries/mediaviewer/impl/build.gradle.kts @@ -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) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt index ccea1a130e..0c4e3cfebc 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryNode.kt @@ -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, 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, + ) + } } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt index c1d1e1ca72..87e7599991 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt @@ -64,7 +64,6 @@ open class MediaGalleryStateProvider : PreviewParameterProvider Unit, onItemClick: (MediaItem.Event) -> Unit, ) { + val presenterFactories = LocalMediaItemPresenterFactories.current LazyColumn( modifier = Modifier.fillMaxSize(), ) { @@ -275,12 +282,16 @@ private fun MediaGalleryFilesList( onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) }, onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) }, ) - is MediaItem.Voice -> VoiceItemView( - item, - onShareClick = { eventSink(MediaGalleryEvents.Share(item)) }, - onDownloadClick = { eventSink(MediaGalleryEvents.SaveOnDisk(item)) }, - onInfoClick = { eventSink(MediaGalleryEvents.OpenInfo(item)) }, - ) + is MediaItem.Voice -> { + val presenter: Presenter = 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 -> { @@ -462,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 = {}, + ) + } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt new file mode 100644 index 0000000000..bf453c33e0 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/FakeTimelineItemPresenterFactories.kt @@ -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 { Presenter { aVoiceMessageState() } }, + ), + ) +) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/LocalMediaItemPresenterFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/LocalMediaItemPresenterFactories.kt new file mode 100644 index 0000000000..8138d4c7f7 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/LocalMediaItemPresenterFactories.kt @@ -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()) +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt new file mode 100644 index 0000000000..7db70901d3 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemEventContentKey.kt @@ -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) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt new file mode 100644 index 0000000000..28b79194c7 --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt @@ -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, 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, MediaItemPresenterFactory<*, *>>, +) { + private val presenters: MutableMap> = 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 rememberPresenter( + content: C, + contentClass: Class, + ): Presenter = remember(content) { + presenters[content]?.let { + @Suppress("UNCHECKED_CAST") + it as Presenter + } ?: factories.getValue(contentClass).let { + @Suppress("UNCHECKED_CAST") + (it as MediaItemPresenterFactory).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 MediaItemPresenterFactories.rememberPresenter( + content: C +): Presenter = rememberPresenter( + content = content, + contentClass = C::class.java +) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt new file mode 100644 index 0000000000..fd621adbfb --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactory.kt @@ -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 { + fun create(content: C): Presenter +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt index 137a94e5d2..472ea6555f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt @@ -20,10 +20,18 @@ 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 @@ -32,15 +40,23 @@ 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, @@ -53,6 +69,7 @@ fun VoiceItemView( .padding(top = 20.dp, start = 16.dp, end = 16.dp), ) { VoiceInfoRow( + state = state, voice = voice, ) val caption = voice.mediaInfo.caption @@ -72,8 +89,13 @@ fun VoiceItemView( @Composable private fun VoiceInfoRow( + state: VoiceMessageState, voice: MediaItem.Voice, ) { + fun playPause() { + state.eventSink(VoiceMessageEvents.PlayPause) + } + Row( modifier = Modifier .clip(RoundedCornerShape(12.dp)) @@ -85,49 +107,155 @@ private fun VoiceInfoRow( .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { - Icon( - modifier = Modifier - .background( - color = ElementTheme.colors.bgCanvasDefault, - shape = CircleShape, - ) - .border( - width = 1.dp, - color = ElementTheme.colors.borderInteractiveSecondary, - shape = CircleShape, - ) - .size(36.dp) - .padding(6.dp), - imageVector = CompoundIcons.PlaySolid(), - tint = ElementTheme.colors.iconSecondary, - contentDescription = null, - ) - voice.duration?.let { - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = voice.duration, - style = ElementTheme.typography.fontBodyMdMedium, - color = ElementTheme.colors.textSecondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + 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), - playbackProgress = 0f, - showCursor = false, + 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( @@ -183,9 +311,24 @@ 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 = {}, + ) +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt new file mode 100644 index 0000000000..9f5a6593ed --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt @@ -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 { + @AssistedFactory + fun interface Factory : MediaItemPresenterFactory { + 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() + } +} diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 8d26082157..1235223337 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -128,6 +128,7 @@ class KonsistPreviewTest { "TimelineVideoWithCaptionRowPreview", "TimelineViewMessageShieldPreview", "UserAvatarColorsPreview", + "VoiceItemViewPlayPreview", ) .assertTrue( additionalMessage = "Functions for Preview should be named like this: Preview. " + From aab4a9de0a49bbc9642183bd6edcf4ff0aa5f07a Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 16 Dec 2024 10:15:23 +0000 Subject: [PATCH 4/5] Update screenshots --- ...ies.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png | 4 ++-- ...ies.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png | 4 ++-- ...ies.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png | 4 ++-- ...ies.mediaviewer.impl.gallery.ui_AudioItemView_Day_3_en.png | 3 --- ...s.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png | 4 ++-- ...s.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png | 4 ++-- ...s.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png | 4 ++-- ...s.mediaviewer.impl.gallery.ui_AudioItemView_Night_3_en.png | 3 --- ...mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png | 3 +++ ...mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png | 3 +++ ...mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png | 3 +++ ...mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png | 3 +++ ...mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png | 3 +++ ...diaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png | 3 +++ ...diaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png | 3 +++ ...diaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png | 3 +++ ...diaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png | 3 +++ ...diaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png | 3 +++ ...ies.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png | 3 +++ ...ies.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png | 3 +++ ...ies.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png | 3 +++ ...ies.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png | 3 +++ ...s.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png | 3 +++ ...s.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png | 3 +++ ...s.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png | 3 +++ ...s.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png | 3 +++ ...ies.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png | 4 ++-- ...s.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png | 4 ++-- 28 files changed, 70 insertions(+), 22 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_3_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png index dc645dea5e..6c0958790b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d07440581dc64c48c047b8895520c7f46213e4806053b6a5fa4b63c15386c8c -size 12410 +oid sha256:9712ec0c9240afdd093e76cbc04143c49807819c31697d957b22e664b66b2dba +size 11333 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png index 6ec428fbfb..b51d1b3801 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9beef21701df1566b27d7b71808de32a87ae439b5213f7e081d821e60add5b36 -size 16302 +oid sha256:d5ec1a0bd7a4d1bd06f3b01c960334503ddac208b6cdedf521dea7c0cd83efd2 +size 15156 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png index aa05357dd7..fd7f6f30fb 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0adeab82d5197daf0eb408f66ee605e0ae3862ac08de0c6a5093a957f724718c -size 39939 +oid sha256:80c113c5a4e71e00e41abc21f22340f8374488047101e4e79472a7549f5bb50f +size 38712 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_3_en.png deleted file mode 100644 index a45dd44c3c..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:835cdc9b99cdd8eedfe2f59032344d07681cd8bbab39a0f15e340abd80c035ff -size 10979 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png index 0fba373d86..311b4ae127 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1813f6e96713fc8c11eca36475ac51b3162f88c71bbe4e9fe7cb0c9a58314ab4 -size 11719 +oid sha256:71e8ea9b852fc15170b61c283248034404761487091ed6290015f4e9c9ca2d3e +size 10919 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png index 87028a2acd..b97ea7edce 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1df8cd0cbef387b05959a423c20cf92605e1c671404aef4ccbb79048d4fc4a6f -size 15518 +oid sha256:527242ca1b585e76247210f2a000e6511bfbe90b0c8f8eaf850f8318b6067d5a +size 14627 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png index 12949183c1..77f9746562 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de63930be672d83e9bc508c6a84911f6a309c2dbac7b608c3bfef5e8a37fc49f -size 38349 +oid sha256:a0c1c64b768ec3491200e38b93ff558873d130a3a5c71b8bd5c3f4a49e3d9137 +size 37273 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_3_en.png deleted file mode 100644 index d80a5bcf08..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_AudioItemView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:836b153cee837cb5c0ba375b587f6bfa6e34eb9f88e4198483f956ebff91048a -size 10234 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png new file mode 100644 index 0000000000..1bc8453928 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d5d02d56e050717a767bcf12d9da7c6ccf1207bedba0a7ec8b0451a668739d9 +size 11032 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png new file mode 100644 index 0000000000..7129e15299 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60f281d970321442e39b350f4f697c7ecfc9bc32000cd19676ea7ed6468ee63a +size 11411 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png new file mode 100644 index 0000000000..070290ee09 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25386b28846ae826701c9e53530cdd1e5fda4d0899673394bf6d81dac1ca751f +size 11057 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png new file mode 100644 index 0000000000..6678d08a23 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:740923d3e740dc98a5d6890f8f8dfb5810606d99e81a4e6597078566194f076d +size 11290 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png new file mode 100644 index 0000000000..f673979846 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e18741850e92314b40da89c9e1cafd1795397960151f7bb4e221ae3866d25f9d +size 11497 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png new file mode 100644 index 0000000000..ee16fdae08 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:979326f2383611db2b1ffa8a2a9d0f2a4fb3296fa57a1240daa97ccf36d886e3 +size 10279 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png new file mode 100644 index 0000000000..afab3fd152 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19c209ec0dca3c3b722af63dcd33444d1074749afd47a8364da4a7a071fce8aa +size 10680 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png new file mode 100644 index 0000000000..1ff42298f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dabd54db78ae93b72cf9ee31fc4b153fe2ccf64869d861fd16995e16494f4d67 +size 10423 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png new file mode 100644 index 0000000000..0cef938ec6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ea12dad2ab5c70aa30bc506343f0fce05e08a2940460ec595216d07a244fc84 +size 10583 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png new file mode 100644 index 0000000000..132a446fae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemViewPlay_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:470f7d0a1ceedcc0f9ade603c590101da8231b24745283976b3528a84e72d721 +size 10743 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png new file mode 100644 index 0000000000..aa1aebbac8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b799d6bed5f1648f1a5196a1c4ccc1a8f8ed49f3eeb5971bd7c342518a1c8ec3 +size 10852 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png new file mode 100644 index 0000000000..267ad90715 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28ead44aa94f2403c4273f38185f0eb21fd9645918829298aee52f9c8061ac95 +size 13003 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png new file mode 100644 index 0000000000..84f6b5081c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae77a7b1bb730842ff0f9608900188d5da78717476f10fb8b640a9c413bf4249 +size 38548 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png new file mode 100644 index 0000000000..086d96caa6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88df6cf8d0a0ca3ae5cdc0e67a21e0c2da793cc88d65fc780830678f5d5f2f24 +size 9218 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png new file mode 100644 index 0000000000..3aa6a4b56d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6dd8d2b4e7d18e3091ac8aca92b8ceb9db6ae15bd228b7a6ce268ed2f7852a53 +size 10113 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png new file mode 100644 index 0000000000..23ca97050b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c711dea48e04cc05c243541ad48e6ed576b5f225ceecb515d49d08c2b2db5e0f +size 12289 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png new file mode 100644 index 0000000000..f9cac2cee7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64f8121851420451628e215393f0835597ea82a67fb2365da2454d52f375bde2 +size 36802 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png new file mode 100644 index 0000000000..27ac795939 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VoiceItemView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80885e7fa1a8b84e1629f64eb4890079d8f4b556c91b415716616ed2d27e5924 +size 8589 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png index 2731706a84..51e4bfa3fe 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:237067f8a59de7f88c313d78716116c5dfe35d9112ad3d42a61f172de2637415 -size 40888 +oid sha256:f8d549d3ef133c621c9e223a3f02f351d8626aeb7b4b05180c2d76b85004cfb9 +size 39459 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png index c560e17b09..fbc749a35a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9304dc29a88d7829aebfb6de1bab40fde20fcaef0fb92efdf9135d4df92c4442 -size 38694 +oid sha256:742abcea0455c0628bbdd2e2cd3367d7a9431fe00cfa0c657a1a95e83c8a7cbf +size 37334 From 7538e9e503462860ed7ebabffd65577ef3595de2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Dec 2024 11:55:20 +0100 Subject: [PATCH 5/5] Fix detekt issue. --- tools/detekt/detekt.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 1e73ef425d..0ebe19bcc4 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -224,6 +224,7 @@ Compose: - LocalCompoundColors - LocalSnackbarDispatcher - LocalCameraPositionState + - LocalMediaItemPresenterFactories - LocalTimelineItemPresenterFactories - LocalRoomMemberProfilesCache - LocalMentionSpanTheme