diff --git a/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/DefaultContentScannerService.kt b/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/DefaultContentScannerService.kt index 5cc8b9e315..2765686f9d 100644 --- a/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/DefaultContentScannerService.kt +++ b/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/DefaultContentScannerService.kt @@ -26,7 +26,7 @@ class DefaultContentScannerService( private val coroutineScope: CoroutineScope, coroutineDispatchers: CoroutineDispatchers, ) : ContentScannerService { - private val context = coroutineDispatchers.io.limitedParallelism(4) + private val dispatcher = coroutineDispatchers.io.limitedParallelism(4) override fun scan(eventId: EventId, mediaSources: List, contentValidationState: ContentValidationState) { for (mediaSource in mediaSources) { @@ -35,7 +35,7 @@ class DefaultContentScannerService( if (currentState != ContentValidationValue.Unknown) continue contentValidationState.update(url, ContentValidationValue.Loading) - coroutineScope.launch(context) { + coroutineScope.launch(dispatcher) { contentScanner.scan(mediaSource) .onSuccess { isValid -> val contentValidationValue = if (isValid) ContentValidationValue.Valid else ContentValidationValue.Invalid diff --git a/features/contentscanner/impl/src/test/kotlin/io/element/android/features/contentscanner/impl/DefaultContentScannerServiceTest.kt b/features/contentscanner/impl/src/test/kotlin/io/element/android/features/contentscanner/impl/DefaultContentScannerServiceTest.kt index accdf0ff37..4bd20ebaf8 100644 --- a/features/contentscanner/impl/src/test/kotlin/io/element/android/features/contentscanner/impl/DefaultContentScannerServiceTest.kt +++ b/features/contentscanner/impl/src/test/kotlin/io/element/android/features/contentscanner/impl/DefaultContentScannerServiceTest.kt @@ -32,7 +32,7 @@ import java.io.FileNotFoundException @OptIn(ExperimentalCoroutinesApi::class) class DefaultContentScannerServiceTest { @Test - fun `scan with no media sources returns valid`() = runTest { + fun `scan with no media sources returns unknown`() = runTest { val scanLambda = lambdaRecorder> { Result.success(true) } val scanner = FakeContentScanner(scan = scanLambda) val contentValidationState = DefaultContentValidationState() @@ -45,7 +45,7 @@ class DefaultContentScannerServiceTest { ) scanLambda.assertions().isNeverCalled() - assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.Valid) + assertThat(contentValidationState.getCurrentOverallState()).isEqualTo(ContentValidationValue.Unknown) } @Test diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt index 1f10a97eda..221ba2ca6f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt @@ -22,8 +22,8 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentVal fun TimelineItemAudioView( content: TimelineItemAudioContent, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, + contentValidationValue: ContentValidationValue, modifier: Modifier = Modifier, - contentValidationValue: ContentValidationValue = ContentValidationValue.Valid, ) { TimelineItemAttachmentView( icon = CompoundIcons.Audio(), @@ -43,5 +43,6 @@ internal fun TimelineItemAudioViewPreview(@PreviewParameter(TimelineItemAudioCon TimelineItemAudioView( content, onContentLayoutChange = {}, + contentValidationValue = ContentValidationValue.Valid, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt index a0deec6d84..cd0632282e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt @@ -27,8 +27,8 @@ import io.element.android.libraries.ui.strings.CommonStrings fun TimelineItemFileView( content: TimelineItemFileContent, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, + contentValidationValue: ContentValidationValue, modifier: Modifier = Modifier, - contentValidationValue: ContentValidationValue = ContentValidationValue.Valid, ) { TimelineItemAttachmentView( icon = CompoundIcons.Attachment(), @@ -48,6 +48,7 @@ internal fun TimelineItemFileViewPreview(@PreviewParameter(TimelineItemFileConte TimelineItemFileView( content, onContentLayoutChange = {}, + contentValidationValue = ContentValidationValue.Valid, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryView.kt index 8c4244a651..c7426f6f8d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryView.kt @@ -288,9 +288,15 @@ private fun GalleryItemCell( Box( modifier = modifier .blurHashBackground(item.blurhash, alpha = 0.9f) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, + .then( + if (itemContentValidationState.isValid()) { + Modifier.combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ) + } else { + Modifier + } ), contentAlignment = Alignment.Center, ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index eb5c507779..3e79fd6fd9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -43,7 +44,9 @@ 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.matrix.ui.media.contentvalidation.ContentValidationState -import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.LocalEventContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopEventContentValidationCache import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.utils.a11y.isTalkbackActive @@ -55,8 +58,8 @@ fun TimelineItemImageView( onContentClick: (() -> Unit)?, onLongClick: (() -> Unit)?, onShowContentClick: () -> Unit, + contentValidationState: ContentValidationState, modifier: Modifier = Modifier, - contentValidationState: ContentValidationState = remember { DefaultContentValidationState() }, ) { val a11yLabel = stringResource(CommonStrings.common_image) val description = content.caption?.let { "$a11yLabel: $it" } ?: a11yLabel @@ -129,52 +132,56 @@ internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageCon onShowContentClick = {}, onContentClick = {}, onLongClick = {}, + contentValidationState = NoopContentValidationState(), ) } @PreviewsDayNight @Composable internal fun TimelineItemImageViewHideMediaContentPreview() = ElementPreview { - TimelineItemImageView( - content = aTimelineItemImageContent(), - hideMediaContent = true, - onShowContentClick = {}, - onContentClick = {}, - onLongClick = {}, - ) + TimelineItemImageView( + content = aTimelineItemImageContent(), + hideMediaContent = true, + onShowContentClick = {}, + onContentClick = {}, + onLongClick = {}, + contentValidationState = NoopContentValidationState(), + ) } @PreviewsDayNight @Composable internal fun ATimelineItemEventRowPreview() = ElementPreview { - Column { - sequenceOf(false, true).forEach { isMine -> + CompositionLocalProvider(LocalEventContentValidationState provides NoopEventContentValidationCache()) { + Column { + sequenceOf(false, true).forEach { isMine -> + ATimelineItemEventRow( + event = aTimelineItemEvent( + isMine = isMine, + content = aTimelineItemImageContent( + filename = "image.jpg", + caption = "A long caption that may wrap into several lines", + width = 40, + height = 20, + aspectRatio = 40f / 20f, + ), + groupPosition = TimelineItemGroupPosition.Last, + ), + ) + } ATimelineItemEventRow( event = aTimelineItemEvent( - isMine = isMine, + isMine = false, content = aTimelineItemImageContent( filename = "image.jpg", - caption = "A long caption that may wrap into several lines", - width = 40, - height = 20, - aspectRatio = 40f / 20f, + caption = "Narrow image with null aspectRatio", + width = 80, + height = 150, + aspectRatio = null, ), groupPosition = TimelineItemGroupPosition.Last, ), ) } - ATimelineItemEventRow( - event = aTimelineItemEvent( - isMine = false, - content = aTimelineItemImageContent( - filename = "image.jpg", - caption = "Narrow image with null aspectRatio", - width = 80, - height = 150, - aspectRatio = null, - ), - groupPosition = TimelineItemGroupPosition.Last, - ), - ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt index 76ffda2a15..7e240f8b51 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt @@ -39,7 +39,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState -import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState import io.element.android.libraries.ui.strings.CommonStrings @@ -52,8 +52,8 @@ fun TimelineItemStickerView( onContentClick: (() -> Unit)?, onLongClick: (() -> Unit)?, onShowClick: () -> Unit, + contentValidationState: ContentValidationState, modifier: Modifier = Modifier, - contentValidationState: ContentValidationState = remember { DefaultContentValidationState() }, ) { val description = content.bestDescription.takeIf { it.isNotEmpty() } ?: stringResource(CommonStrings.common_image) @@ -130,5 +130,6 @@ internal fun TimelineItemStickerViewPreview(@PreviewParameter(TimelineItemSticke onContentClick = {}, onLongClick = {}, onShowClick = {}, + contentValidationState = NoopContentValidationState(), ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index ce97ecc775..b47220fa5e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -51,7 +51,7 @@ import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState -import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.utils.a11y.isTalkbackActive @@ -63,8 +63,8 @@ fun TimelineItemVideoView( onContentClick: (() -> Unit)?, onLongClick: (() -> Unit)?, onShowContentClick: () -> Unit, + contentValidationState: ContentValidationState, modifier: Modifier = Modifier, - contentValidationState: ContentValidationState = remember { DefaultContentValidationState() }, ) { val isTalkbackActive = isTalkbackActive() val a11yLabel = stringResource(CommonStrings.common_video) @@ -157,6 +157,7 @@ internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoCon onShowContentClick = {}, onContentClick = {}, onLongClick = {}, + contentValidationState = NoopContentValidationState(), ) } @@ -169,5 +170,6 @@ internal fun TimelineItemVideoViewHideMediaContentPreview() = ElementPreview { onShowContentClick = {}, onContentClick = {}, onLongClick = {}, + contentValidationState = NoopContentValidationState(), ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index 67f1cbfa96..15387f51c0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -64,8 +64,8 @@ fun TimelineItemVoiceView( state: VoiceMessageState, content: TimelineItemVoiceContent, onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit, + contentValidationValue: ContentValidationValue, modifier: Modifier = Modifier, - contentValidationValue: ContentValidationValue = ContentValidationValue.Valid, ) { fun playPause() { state.eventSink(VoiceMessageEvent.PlayPause) @@ -289,6 +289,7 @@ internal fun TimelineItemVoiceViewPreview( state = timelineItemVoiceViewParameters.state, content = timelineItemVoiceViewParameters.content, onContentLayoutChange = {}, + contentValidationValue = ContentValidationValue.Valid, ) } @@ -302,6 +303,7 @@ internal fun TimelineItemVoiceViewUnifiedPreview() = ElementPreview { state = it.state, content = it.content, onContentLayoutChange = {}, + contentValidationValue = ContentValidationValue.Valid, ) } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/contentvalidation/ContentValidationState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/contentvalidation/ContentValidationState.kt index c0afa69d1a..bbaf52f5b0 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/contentvalidation/ContentValidationState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/contentvalidation/ContentValidationState.kt @@ -116,6 +116,7 @@ class DefaultContentValidationState( private fun calculateOverallState(states: Map): ContentValidationValue { return when { + states.values.isEmpty() -> ContentValidationValue.Unknown states.values.any { it is ContentValidationValue.Invalid } -> ContentValidationValue.Invalid states.values.any { it is ContentValidationValue.Loading } -> ContentValidationValue.Loading states.values.any { it is ContentValidationValue.UnrecoverableError } -> states.values.first { it is ContentValidationValue.UnrecoverableError } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/contentvalidation/LocalEventContentValidationState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/contentvalidation/LocalEventContentValidationState.kt index ab7f6da042..f5277a3537 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/contentvalidation/LocalEventContentValidationState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/contentvalidation/LocalEventContentValidationState.kt @@ -26,8 +26,10 @@ val LocalEventContentValidationState = staticCompositionLocalOf { @@ -138,6 +144,7 @@ class EventItemFactory( mediaSource = c.source, thumbnailSource = c.info?.thumbnailSource, blurHash = c.info?.blurhash, + validationState = validationState, ) } is GalleryItemType.Audio -> { @@ -156,6 +163,7 @@ class EventItemFactory( dateSentFull = dateSentFull, ), mediaSource = c.source, + validationState = validationState, ) } is GalleryItemType.File -> { @@ -174,6 +182,7 @@ class EventItemFactory( dateSentFull = dateSentFull, ), mediaSource = c.source, + validationState = validationState, ) } is GalleryItemType.Other -> null @@ -200,6 +209,7 @@ class EventItemFactory( duration = null, ), mediaSource = type.source, + validationState = validationState, )) is FileMessageType -> listOf(MediaItem.File( id = currentTimelineItem.uniqueId, @@ -221,6 +231,7 @@ class EventItemFactory( duration = null, ), mediaSource = type.source, + validationState = validationState, // TODO We may want to add a thumbnailSource and set it to type.info?.thumbnailSource )) is ImageMessageType -> listOf(MediaItem.Image( @@ -245,6 +256,7 @@ class EventItemFactory( mediaSource = type.source, thumbnailSource = type.info?.thumbnailSource, blurHash = type.info?.blurhash, + validationState = validationState, )) is StickerMessageType -> listOf(MediaItem.Image( id = currentTimelineItem.uniqueId, @@ -268,6 +280,7 @@ class EventItemFactory( mediaSource = type.source, thumbnailSource = type.info?.thumbnailSource, blurHash = type.info?.blurhash, + validationState = validationState, )) is VideoMessageType -> listOf(MediaItem.Video( id = currentTimelineItem.uniqueId, @@ -291,6 +304,7 @@ class EventItemFactory( mediaSource = type.source, thumbnailSource = type.info?.thumbnailSource, blurHash = type.info?.blurhash, + validationState = validationState, )) is VoiceMessageType -> listOf(MediaItem.Voice( id = currentTimelineItem.uniqueId, @@ -312,6 +326,7 @@ class EventItemFactory( duration = type.info?.duration?.inWholeMilliseconds?.toHumanReadableDuration(), ), mediaSource = type.source, + validationState = validationState, )) } } 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 25e27330b4..2add4630e7 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 @@ -20,6 +20,7 @@ import androidx.compose.runtime.setValue import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject +import io.element.android.features.contentscanner.api.ContentScannerService import io.element.android.libraries.androidutils.R import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -43,7 +44,11 @@ import io.element.android.libraries.mediaviewer.impl.model.eventId import io.element.android.libraries.mediaviewer.impl.model.mediaInfo import io.element.android.libraries.mediaviewer.impl.model.mediaPermissions import io.element.android.libraries.mediaviewer.impl.model.mediaSource +import io.element.android.libraries.mediaviewer.impl.model.thumbnailSource import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @AssistedInject @@ -55,6 +60,7 @@ class MediaGalleryPresenter( private val mediaLoader: MatrixMediaLoader, private val localMediaActions: LocalMediaActions, private val snackbarDispatcher: SnackbarDispatcher, + private val contentScannerService: ContentScannerService, ) : Presenter { @AssistedFactory interface Factory { @@ -74,6 +80,17 @@ class MediaGalleryPresenter( val groupedMediaItems by remember { mediaGalleryDataSource.groupedMediaItemsFlow() + .onEach { groupedItems -> + if (groupedItems is AsyncData.Success) { + val items = groupedItems.data.getItems(mode) + items.forEach { item -> + if (item is MediaItem.Event) { + val eventId = item.eventId() ?: return@forEach + coroutineScope.validateMedia(eventId, item) + } + } + } + } } .collectAsState(AsyncData.Uninitialized) @@ -220,6 +237,21 @@ class MediaGalleryPresenter( } } + private fun CoroutineScope.validateMedia(eventId: EventId, mediaItem: MediaItem.Event) { + launch { + val thumbnailSource = mediaItem.thumbnailSource() + val mediaSource = mediaItem.mediaSource() + val contentValidationState = mediaItem.validationState + val currentState = contentValidationState.overallStateFlow.first() + + if (currentState.isLoading() || currentState.isValid()) { + return@launch + } + + contentScannerService.scan(eventId, listOfNotNull(thumbnailSource, mediaSource).distinct(), contentValidationState) + } + } + private fun mediaActionsError(throwable: Throwable): Int { return if (throwable is ActivityNotFoundException) { R.string.error_no_compatible_app_found 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 75b849cd0c..43a361ac29 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 @@ -13,6 +13,8 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.media.WaveFormSamples import io.element.android.libraries.designsystem.preview.ROOM_NAME import io.element.android.libraries.matrix.api.core.UniqueId +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState import io.element.android.libraries.mediaviewer.impl.details.aMediaBottomSheetStateDetails import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems @@ -47,9 +49,20 @@ open class MediaGalleryStateProvider : PreviewParameterProvider + val contentValidationState = remember(item is MediaItem.Event) { + if (item is MediaItem.Event) { + item.validationState + } else { + NoopContentValidationState() + } + } + val contentValidationValue by contentValidationState.collectOverallState() when (item) { is MediaItem.File -> FileItemView( modifier = Modifier.animateItem(), @@ -323,6 +342,7 @@ private fun MediaGalleryFilesList( onLongClick = { eventSink(MediaGalleryEvent.OpenInfo(item)) }, + contentValidationState = contentValidationValue, ) is MediaItem.Audio -> AudioItemView( modifier = Modifier.animateItem(), @@ -331,6 +351,7 @@ private fun MediaGalleryFilesList( onLongClick = { eventSink(MediaGalleryEvent.OpenInfo(item)) }, + contentValidationValue = contentValidationValue, ) is MediaItem.Voice -> { val presenter: Presenter = presenterFactories.rememberPresenter(item) @@ -341,16 +362,17 @@ private fun MediaGalleryFilesList( onLongClick = { eventSink(MediaGalleryEvent.OpenInfo(item)) }, + contentValidationValue = contentValidationValue, ) } + is MediaItem.Image, + is MediaItem.Video -> { + // Should not happen + } is MediaItem.DateSeparator -> DateItemView( modifier = Modifier.animateItem(), item = item ) - is MediaItem.Image, - is MediaItem.Video -> { - // Should not happen - } is MediaItem.LoadingIndicator -> LoadingMoreIndicator( modifier = Modifier.animateItem(), item = item, @@ -387,11 +409,48 @@ private fun MediaGalleryImageGrid( key = { it.id() }, contentType = { it::class.java }, ) { item -> + val blurHash = when (item) { + is MediaItem.Image -> item.blurHash + is MediaItem.Video -> item.blurHash + else -> null + } + + val blurHashBackgroundModifier = if (blurHash != null) { + Modifier.blurHashBackground(blurHash) + } else { + Modifier + } + + val currentValidationState by when (item) { + is MediaItem.Event -> item.validationState.collectOverallState() + else -> remember { mutableStateOf(ContentValidationValue.Unknown) } + } + when (item) { - is MediaItem.DateSeparator -> DateItemView( - modifier = Modifier.animateItem(), - item = item, - ) + is MediaItem.Event if !currentValidationState.isValidated() -> { + Box(modifier = blurHashBackgroundModifier.aspectRatio(1f), contentAlignment = Alignment.Center) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp, + ) + } + } + is MediaItem.Event if currentValidationState.isInvalid() -> { + Box( + modifier = Modifier + .clickable { onItemClick(item) } + .background(ElementTheme.colors.bgCriticalSubtle) + .aspectRatio(1f), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = CompoundIcons.Error(), + tint = ElementTheme.colors.iconCriticalPrimary, + contentDescription = null, + ) + } + } is MediaItem.Audio -> { // Should not happen } @@ -402,7 +461,7 @@ private fun MediaGalleryImageGrid( // Should not happen } is MediaItem.Image -> ImageItemView( - modifier = Modifier.animateItem(), + modifier = blurHashBackgroundModifier.animateItem(), image = item, onClick = { onItemClick(item) }, onLongClick = { @@ -410,13 +469,17 @@ private fun MediaGalleryImageGrid( }, ) is MediaItem.Video -> VideoItemView( - modifier = Modifier.animateItem(), + modifier = blurHashBackgroundModifier.animateItem(), video = item, onClick = { onItemClick(item) }, onLongClick = { eventSink(MediaGalleryEvent.OpenInfo(item)) }, ) + is MediaItem.DateSeparator -> DateItemView( + modifier = Modifier.animateItem(), + item = item, + ) is MediaItem.LoadingIndicator -> LoadingMoreIndicator( modifier = Modifier.animateItem(), item = 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 ed4e59d631..cda8422e33 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,117 +8,38 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable 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.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -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.core.extensions.withBrackets -import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction 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.Text +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue import io.element.android.libraries.mediaviewer.impl.model.MediaItem -import io.element.android.libraries.ui.strings.CommonStrings @Composable fun AudioItemView( audio: MediaItem.Audio, onClick: () -> Unit, onLongClick: () -> Unit, + contentValidationValue: ContentValidationValue, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + GalleryFileListItem( + modifier = modifier, + contentValidationValue = contentValidationValue, + caption = audio.mediaInfo.caption, ) { - Spacer(modifier = Modifier.height(20.dp)) - FilenameRow( - audio = audio, + GalleryFileListItemContent( + name = audio.mediaInfo.filename, + formattedSize = audio.mediaInfo.formattedFileSize, + icon = CompoundIcons.Audio(), + isValidating = contentValidationValue.isLoading(), onClick = onClick, onLongClick = onLongClick, ) - val caption = audio.mediaInfo.caption - if (caption != null) { - CaptionView(caption) - } else { - Spacer(modifier = Modifier.height(20.dp)) - } - HorizontalDivider() - } -} - -@Composable -private fun FilenameRow( - audio: MediaItem.Audio, - onClick: () -> Unit, - onLongClick: () -> Unit, -) { - Row( - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background( - color = ElementTheme.colors.bgSubtleSecondary, - shape = RoundedCornerShape(12.dp), - ) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), - ) - .onKeyboardContextMenuAction(onLongClick) - .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), - imageVector = CompoundIcons.Audio(), - contentDescription = null, - ) - Spacer(modifier = Modifier.width(8.dp)) - 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 = formattedSize.withBrackets(), - style = ElementTheme.typography.fontBodyLgRegular, - color = ElementTheme.colors.textPrimary, - ) - } } } @@ -127,9 +48,22 @@ private fun FilenameRow( internal fun AudioItemViewPreview( @PreviewParameter(MediaItemAudioProvider::class) audio: MediaItem.Audio, ) = ElementPreview { - AudioItemView( - audio = audio, - onClick = {}, - onLongClick = {}, - ) + val states = remember { + listOf( + ContentValidationValue.Valid, + ContentValidationValue.Loading, + ContentValidationValue.Invalid, + ContentValidationValue.UnrecoverableError(Throwable("Unrecoverable error")), + ) + } + Column { + for (state in states) { + AudioItemView( + contentValidationValue = state, + audio = audio, + onClick = {}, + onLongClick = {}, + ) + } + } } 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 7738e4c1e4..b552c374b9 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 @@ -8,117 +8,38 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable 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.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -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.core.extensions.withBrackets -import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction 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.Text +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue import io.element.android.libraries.mediaviewer.impl.model.MediaItem -import io.element.android.libraries.ui.strings.CommonStrings @Composable fun FileItemView( file: MediaItem.File, onClick: () -> Unit, onLongClick: () -> Unit, + contentValidationState: ContentValidationValue, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + GalleryFileListItem( + modifier = modifier, + contentValidationValue = contentValidationState, + caption = file.mediaInfo.caption ) { - Spacer(modifier = Modifier.height(20.dp)) - FilenameRow( - file = file, + GalleryFileListItemContent( + name = file.mediaInfo.filename, + formattedSize = file.mediaInfo.formattedFileSize, + icon = CompoundIcons.Attachment(), onClick = onClick, + isValidating = contentValidationState.isLoading(), onLongClick = onLongClick, ) - val caption = file.mediaInfo.caption - if (caption != null) { - CaptionView(caption) - } else { - Spacer(modifier = Modifier.height(20.dp)) - } - HorizontalDivider() - } -} - -@Composable -private fun FilenameRow( - file: MediaItem.File, - onClick: () -> Unit, - onLongClick: () -> Unit, -) { - Row( - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background( - color = ElementTheme.colors.bgSubtleSecondary, - shape = RoundedCornerShape(12.dp), - ) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), - ) - .onKeyboardContextMenuAction(onLongClick) - .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), - imageVector = CompoundIcons.Attachment(), - contentDescription = null, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = file.mediaInfo.filename, - modifier = Modifier.weight(1f), - style = ElementTheme.typography.fontBodyLgRegular, - color = ElementTheme.colors.textPrimary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - val formattedSize = file.mediaInfo.formattedFileSize - if (formattedSize.isNotEmpty()) { - Text( - text = formattedSize.withBrackets(), - style = ElementTheme.typography.fontBodyLgRegular, - color = ElementTheme.colors.textPrimary, - ) - } } } @@ -127,9 +48,22 @@ private fun FilenameRow( internal fun FileItemViewPreview( @PreviewParameter(MediaItemFileProvider::class) file: MediaItem.File, ) = ElementPreview { - FileItemView( - file = file, - onClick = {}, - onLongClick = {}, - ) + val states = remember { + listOf( + ContentValidationValue.Valid, + ContentValidationValue.Loading, + ContentValidationValue.Invalid, + ContentValidationValue.UnrecoverableError(Throwable("Unrecoverable error")), + ) + } + Column { + for (state in states) { + FileItemView( + file = file, + onClick = {}, + onLongClick = {}, + contentValidationState = state, + ) + } + } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/GalleryFileListItem.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/GalleryFileListItem.kt new file mode 100644 index 0000000000..383f32653e --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/GalleryFileListItem.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files 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.combinedClickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.core.extensions.withBrackets +import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction +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.Text +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue +import io.element.android.libraries.matrix.ui.media.contentvalidation.InvalidContentView +import io.element.android.libraries.matrix.ui.media.contentvalidation.NotFoundContentView +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun GalleryFileListItem( + contentValidationValue: ContentValidationValue, + caption: String?, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Spacer(modifier = Modifier.height(20.dp)) + + when (contentValidationValue) { + ContentValidationValue.Invalid -> InvalidContentView( + modifier = invalidContentModifier(), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), + ) + is ContentValidationValue.UnrecoverableError -> NotFoundContentView( + modifier = invalidContentModifier(), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), + ) + else -> { + content() + } + } + + if (caption != null) { + CaptionView(caption) + } else { + Spacer(modifier = Modifier.height(20.dp)) + } + HorizontalDivider() + } +} + +@Composable +private fun invalidContentModifier(): Modifier { + return Modifier + .border(width = 1.dp, color = ElementTheme.colors.borderCriticalSubtle, shape = RoundedCornerShape(12.dp)) + .clip(RoundedCornerShape(12.dp)) + .background(ElementTheme.colors.bgCriticalSubtle) +} + +@Composable +internal fun GalleryFileListItemContent( + name: String, + formattedSize: String, + icon: ImageVector, + isValidating: Boolean, + onClick: () -> Unit, + onLongClick: () -> Unit, +) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background( + color = ElementTheme.colors.bgSubtleSecondary, + shape = RoundedCornerShape(12.dp), + ) + .combinedClickable( + onClick = onClick.takeIf { !isValidating } ?: {}, + onLongClick = onLongClick.takeIf { !isValidating } ?: {}, + onLongClickLabel = stringResource(CommonStrings.action_open_context_menu), + ) + .onKeyboardContextMenuAction(onLongClick) + .fillMaxWidth() + .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val iconModifier = Modifier + .background( + color = ElementTheme.colors.bgActionSecondaryRest, + shape = CircleShape, + ) + .size(32.dp) + .padding(6.dp) + + if (isValidating) { + CircularProgressIndicator( + modifier = iconModifier, + strokeWidth = 2.dp, + ) + } else { + Icon( + modifier = iconModifier, + imageVector = icon, + contentDescription = null, + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = name, + modifier = Modifier.weight(1f), + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (formattedSize.isNotEmpty()) { + Text( + text = formattedSize.withBrackets(), + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPrimary, + ) + } + } +} 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 25e8a88dfe..4dfd1c18c8 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 @@ -22,7 +22,7 @@ class MediaItemAudioProvider : PreviewParameterProvider { caption = "A caption", ), aMediaItemAudio( - caption = loremIpsum, + caption = loremIpsum.take(80), ), ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt index 7419ae3d44..e64b10adb4 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt @@ -22,7 +22,7 @@ class MediaItemFileProvider : PreviewParameterProvider { caption = "A caption", ), aMediaItemFile( - caption = loremIpsum, + caption = loremIpsum.take(80), ), ) } 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 index 4b23d28795..56c1c4bb32 100644 --- 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 @@ -22,7 +22,7 @@ class MediaItemVoiceProvider : PreviewParameterProvider { caption = "A caption", ), aMediaItemVoice( - caption = loremIpsum, + caption = loremIpsum.take(80), ), aMediaItemVoice( waveform = emptyList(), 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 dbbeb28b80..6c9df5fc48 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 @@ -45,10 +45,10 @@ import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenu 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.matrix.ui.media.contentvalidation.ContentValidationValue import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVoice import io.element.android.libraries.ui.strings.CommonStrings @@ -64,26 +64,20 @@ fun VoiceItemView( state: VoiceMessageState, voice: MediaItem.Voice, onLongClick: () -> Unit, + contentValidationValue: ContentValidationValue, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + GalleryFileListItem( + modifier = modifier, + contentValidationValue = contentValidationValue, + caption = voice.mediaInfo.caption, ) { - Spacer(modifier = Modifier.height(20.dp)) VoiceInfoRow( state = state, voice = voice, + isValidating = contentValidationValue.isLoading(), onLongClick = onLongClick, ) - val caption = voice.mediaInfo.caption - if (caption != null) { - CaptionView(caption) - } else { - Spacer(modifier = Modifier.height(16.dp)) - } - HorizontalDivider() } } @@ -91,6 +85,7 @@ fun VoiceItemView( private fun VoiceInfoRow( state: VoiceMessageState, voice: MediaItem.Voice, + isValidating: Boolean, onLongClick: () -> Unit, ) { fun playPause() { @@ -114,12 +109,21 @@ private fun VoiceInfoRow( .padding(start = 12.dp, end = 36.dp, top = 8.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { - when (state.buttonType) { - VoiceMessageState.ButtonType.Play -> PlayButton(onClick = ::playPause) - VoiceMessageState.ButtonType.Pause -> PauseButton(onClick = ::playPause) - VoiceMessageState.ButtonType.Downloading -> ProgressButton() - VoiceMessageState.ButtonType.Retry -> RetryButton(onClick = ::playPause) - VoiceMessageState.ButtonType.Disabled -> PlayButton(onClick = {}, enabled = false) + if (isValidating) { + CustomIconButton(onClick = {}, enabled = false) { + CircularProgressIndicator( + strokeWidth = 2.dp, + modifier = Modifier.size(16.dp) + ) + } + } else { + when (state.buttonType) { + VoiceMessageState.ButtonType.Play -> PlayButton(onClick = ::playPause) + VoiceMessageState.ButtonType.Pause -> PauseButton(onClick = ::playPause) + VoiceMessageState.ButtonType.Downloading -> ProgressButton() + VoiceMessageState.ButtonType.Retry -> RetryButton(onClick = ::playPause) + VoiceMessageState.ButtonType.Disabled -> PlayButton(onClick = {}, enabled = false) + } } Spacer(Modifier.width(8.dp)) Column( @@ -128,7 +132,7 @@ private fun VoiceInfoRow( ) { PlaybackSpeedButton( speed = state.playbackSpeed, - onClick = { state.eventSink(VoiceMessageEvent.ChangePlaybackSpeed) }, + onClick = { state.eventSink(VoiceMessageEvent.ChangePlaybackSpeed) }.takeIf { !isValidating } ?: {}, ) Text( text = if (state.progress > 0f) state.time else voice.mediaInfo.duration ?: state.time, @@ -149,7 +153,7 @@ private fun VoiceInfoRow( onSeek = { state.eventSink(VoiceMessageEvent.Seek(it)) }, - seekEnabled = true, + seekEnabled = !isValidating, ) } } @@ -277,11 +281,24 @@ private fun CustomIconButton( internal fun VoiceItemViewPreview( @PreviewParameter(MediaItemVoiceProvider::class) voice: MediaItem.Voice, ) = ElementPreview { - VoiceItemView( - state = aVoiceMessageState(), - voice = voice, - onLongClick = {}, - ) + val states = remember { + listOf( + ContentValidationValue.Valid, + ContentValidationValue.Loading, + ContentValidationValue.Invalid, + ContentValidationValue.UnrecoverableError(Throwable("Unrecoverable error")), + ) + } + Column { + for (state in states) { + VoiceItemView( + state = aVoiceMessageState(), + voice = voice, + onLongClick = {}, + contentValidationValue = state, + ) + } + } } @PreviewsDayNight @@ -289,9 +306,22 @@ internal fun VoiceItemViewPreview( internal fun VoiceItemViewPlayPreview( @PreviewParameter(VoiceMessageStateProvider::class) state: VoiceMessageState, ) = ElementPreview { - VoiceItemView( - state = state, - voice = aMediaItemVoice(), - onLongClick = {}, - ) + val validationState = remember { + listOf( + ContentValidationValue.Valid, + ContentValidationValue.Loading, + ContentValidationValue.Invalid, + ContentValidationValue.UnrecoverableError(Throwable("Unrecoverable error")), + ) + } + Column { + for (validationState in validationState) { + VoiceItemView( + state = state, + voice = aMediaItemVoice(), + onLongClick = {}, + contentValidationValue = validationState, + ) + } + } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt index d6484cd5a4..b5c65fa609 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/video/MediaVideoView.kt @@ -254,29 +254,31 @@ private fun ExoPlayerMediaVideoView( }, ) } - MediaPlayerControllerView( - state = mediaPlayerControllerState, - onTogglePlay = { - autoHideController++ - exoPlayer.togglePlay() - }, - onSeekChange = { - autoHideController++ - mediaPlayerControllerState = mediaPlayerControllerState.copy( - seekingToMillis = it.toLong(), - ) - exoPlayer.seekToEnsurePlaying(it.toLong()) - }, - onToggleMute = { - autoHideController++ - exoPlayer.volume = if (exoPlayer.volume == 1f) 0f else 1f - }, - audioFocus = audioFocus, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(bottom = bottomPaddingInPixels.toDp()), - ) + if (localMedia != null) { + MediaPlayerControllerView( + state = mediaPlayerControllerState, + onTogglePlay = { + autoHideController++ + exoPlayer.togglePlay() + }, + onSeekChange = { + autoHideController++ + mediaPlayerControllerState = mediaPlayerControllerState.copy( + seekingToMillis = it.toLong(), + ) + exoPlayer.seekToEnsurePlaying(it.toLong()) + }, + onToggleMute = { + autoHideController++ + exoPlayer.volume = if (exoPlayer.volume == 1f) 0f else 1f + }, + audioFocus = audioFocus, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = bottomPaddingInPixels.toDp()), + ) + } } LaunchedEffect(exoPlayer.isPlaying) { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItem.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItem.kt index 5a0b56a9b7..5367751a19 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItem.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItem.kt @@ -13,6 +13,8 @@ import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue import io.element.android.libraries.mediaviewer.api.MediaInfo sealed interface MediaItem { @@ -27,7 +29,9 @@ sealed interface MediaItem { val timestamp: Long, ) : MediaItem - sealed interface Event : MediaItem + sealed interface Event : MediaItem { + val validationState: ContentValidationState + } data class Image( val id: UniqueId, @@ -36,6 +40,7 @@ sealed interface MediaItem { val mediaSource: MediaSource, val thumbnailSource: MediaSource?, val blurHash: String?, + override val validationState: ContentValidationState, ) : Event { val thumbnailMediaRequestData: MediaRequestData get() = MediaRequestData(thumbnailSource ?: mediaSource, MediaRequestData.Kind.Thumbnail(100)) @@ -48,6 +53,7 @@ sealed interface MediaItem { val mediaSource: MediaSource, val thumbnailSource: MediaSource?, val blurHash: String?, + override val validationState: ContentValidationState, ) : Event { val thumbnailMediaRequestData: MediaRequestData get() = MediaRequestData(thumbnailSource ?: mediaSource, MediaRequestData.Kind.Thumbnail(100)) @@ -58,6 +64,7 @@ sealed interface MediaItem { val eventId: EventId?, val mediaInfo: MediaInfo, val mediaSource: MediaSource, + override val validationState: ContentValidationState, ) : Event data class Voice( @@ -65,6 +72,7 @@ sealed interface MediaItem { val eventId: EventId?, val mediaInfo: MediaInfo, val mediaSource: MediaSource, + override val validationState: ContentValidationState, ) : Event data class File( @@ -72,6 +80,7 @@ sealed interface MediaItem { val eventId: EventId?, val mediaInfo: MediaInfo, val mediaSource: MediaSource, + override val validationState: ContentValidationState, ) : Event } @@ -134,3 +143,8 @@ fun MediaItem.Event.blurHash(): String? { else -> null } } + +fun MediaItem.isMediaValid(): Boolean? = when (this) { + is MediaItem.Event -> validationState.getCurrentOverallState() == ContentValidationValue.Valid + else -> null +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItemFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItemFactories.kt index 96059cfd1f..d625ebe398 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItemFactories.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/model/MediaItemFactories.kt @@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo import io.element.android.libraries.mediaviewer.api.aVoiceMediaInfo @@ -25,6 +27,8 @@ fun aMediaItemImage( eventId: EventId? = null, senderId: UserId? = null, mediaSourceUrl: String = "", + validationState: ContentValidationState = NoopContentValidationState(), + blurHash: String? = null, ): MediaItem.Image { return MediaItem.Image( id = id, @@ -34,7 +38,8 @@ fun aMediaItemImage( ), mediaSource = MediaSource(mediaSourceUrl), thumbnailSource = null, - blurHash = null, + blurHash = blurHash, + validationState = validationState, ) } @@ -42,6 +47,7 @@ fun aMediaItemVideo( id: UniqueId = UniqueId("videoId"), mediaSource: MediaSource = MediaSource(""), duration: String? = "1:23", + validationState: ContentValidationState = NoopContentValidationState(), ): MediaItem.Video { return MediaItem.Video( id = id, @@ -52,6 +58,7 @@ fun aMediaItemVideo( mediaSource = mediaSource, thumbnailSource = null, blurHash = null, + validationState = validationState, ) } @@ -60,6 +67,7 @@ fun aMediaItemFile( eventId: EventId? = null, filename: String = "filename", caption: String? = null, + validationState: ContentValidationState = NoopContentValidationState(), ): MediaItem.File { return MediaItem.File( id = id, @@ -69,6 +77,7 @@ fun aMediaItemFile( caption = caption, ), mediaSource = MediaSource(""), + validationState = validationState, ) } @@ -77,6 +86,7 @@ fun aMediaItemAudio( eventId: EventId? = null, filename: String = "filename", caption: String? = null, + validationState: ContentValidationState = NoopContentValidationState(), ): MediaItem.Audio { return MediaItem.Audio( id = id, @@ -86,6 +96,7 @@ fun aMediaItemAudio( caption = caption, ), mediaSource = MediaSource(""), + validationState = validationState, ) } @@ -95,6 +106,7 @@ fun aMediaItemVoice( caption: String? = null, duration: String? = "1:23", waveform: List = WaveFormSamples.realisticWaveForm, + validationState: ContentValidationState = NoopContentValidationState(), ): MediaItem.Voice { return MediaItem.Voice( id = id, @@ -106,6 +118,7 @@ fun aMediaItemVoice( waveForm = waveform, ), mediaSource = MediaSource(""), + validationState = validationState, ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSource.kt index d254985ebf..c8b1de07aa 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSource.kt @@ -11,6 +11,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState import io.element.android.libraries.mediaviewer.api.GalleryInfo import io.element.android.libraries.mediaviewer.api.GalleryItemData import io.element.android.libraries.mediaviewer.api.MediaInfo @@ -37,6 +38,7 @@ class GalleryMediaGalleryDataSource( eventId: EventId?, galleryItems: List, galleryInfo: GalleryInfo, + contentValidationState: ContentValidationState, ): GalleryMediaGalleryDataSource { val mixedItems = mutableListOf() galleryItems.forEachIndexed { index, galleryItem -> @@ -64,18 +66,21 @@ class GalleryMediaGalleryDataSource( mediaSource = galleryItem.mediaSource, thumbnailSource = galleryItem.thumbnailSource, blurHash = galleryItem.blurHash, + validationState = contentValidationState, ) GalleryItemData.Type.Audio -> MediaItem.Audio( id = id, eventId = eventId, mediaInfo = itemMediaInfo, mediaSource = galleryItem.mediaSource, + validationState = contentValidationState, ) GalleryItemData.Type.File -> MediaItem.File( id = id, eventId = eventId, mediaInfo = itemMediaInfo, mediaSource = galleryItem.mediaSource, + validationState = contentValidationState, ) GalleryItemData.Type.Image -> MediaItem.Image( id = id, @@ -84,6 +89,7 @@ class GalleryMediaGalleryDataSource( mediaSource = galleryItem.mediaSource, thumbnailSource = galleryItem.thumbnailSource, blurHash = galleryItem.blurHash, + validationState = contentValidationState, ) } mixedItems.add(mediaItem) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt index 7cbeafa0b2..8b48955502 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.rememberUpdatedState +import io.element.android.features.contentscanner.api.ContentScannerService import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.matrix.api.core.EventId @@ -24,6 +25,9 @@ import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.EventContentValidationCache import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint.MediaViewerMode import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory @@ -57,6 +61,8 @@ class MediaViewerDataSource( private val localMediaFactory: LocalMediaFactory, private val systemClock: SystemClock, private val pagerKeysHandler: PagerKeysHandler, + private val contentScannerService: ContentScannerService, + private val contentValidationCache: EventContentValidationCache, ) { // List of media files that are currently being loaded private val mediaFiles: ConcurrentHashMap = ConcurrentHashMap() @@ -71,6 +77,9 @@ class MediaViewerDataSource( private val localMediaStates: MutableMap>> = mutableMapOf() + private val mediaValidationState: MutableMap = + mutableMapOf() + fun setup(coroutineScope: CoroutineScope) { galleryDataSource.start(coroutineScope) } @@ -162,6 +171,9 @@ class MediaViewerDataSource( val localMedia = localMediaStates.getOrPut(sourceUrl) { mutableStateOf(AsyncData.Uninitialized) } + val validationState = mediaValidationState.getOrPut(sourceUrl) { + mediaItem.eventId()?.let { contentValidationCache[it] } ?: DefaultContentValidationState() + } add( MediaViewerPageData.MediaViewerData( eventId = mediaItem.eventId(), @@ -170,6 +182,7 @@ class MediaViewerDataSource( thumbnailSource = mediaItem.thumbnailSource(), downloadedMedia = localMedia, pagerKey = pagerKeysHandler.getKey(mediaItem), + validationState = validationState, ) ) } @@ -229,6 +242,20 @@ class MediaViewerDataSource( } } + fun validateMedia( + eventId: EventId, + mediaSource: MediaSource, + ) { + val validationState = mediaValidationState[mediaSource.safeUrl] ?: return + val currentState = validationState.getCurrentMediaState(mediaSource.safeUrl) + + if (currentState.isLoading() || currentState.isValid()) { + return + } + + contentScannerService.scan(eventId, listOf(mediaSource), validationState) + } + fun cancelLoadingMedia(data: MediaViewerPageData.MediaViewerData) { if (localMediaStates[data.mediaSource.safeUrl]?.value?.isLoading() == true) { Timber.d("cancelLoadingMedia for ${data.eventId}") diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt index 53d287075d..21769f81be 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvent.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.Timeline sealed interface MediaViewerEvent { @@ -30,4 +31,5 @@ sealed interface MediaViewerEvent { data class OnNavigateTo(val index: Int) : MediaViewerEvent data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvent data class CancelLoadingMedia(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent + data class ValidateMedia(val eventId: EventId, val mediaSource: MediaSource) : MediaViewerEvent } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt index 2298981543..e53153ce19 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt @@ -22,6 +22,7 @@ import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.compound.colors.SemanticColorsLightDark import io.element.android.compound.theme.ForcedDarkElementTheme +import io.element.android.features.contentscanner.api.ContentScannerService import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.viewfolder.api.TextFileViewer import io.element.android.libraries.architecture.callback @@ -33,6 +34,8 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.ui.media.contentvalidation.EventContentValidationCache +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.impl.datasource.FocusedTimelineMediaGalleryDataSourceFactory @@ -57,6 +60,8 @@ class MediaViewerNode( private val audioFocus: AudioFocus, private val sessionId: SessionId, private val enterpriseService: EnterpriseService, + private val contentScannerService: ContentScannerService, + private val contentValidationCache: EventContentValidationCache, ) : Node(buildContext, plugins = plugins), MediaViewerNavigator { private val callback: MediaViewerEntryPoint.Callback = callback() @@ -76,12 +81,13 @@ class MediaViewerNode( private val mediaGallerySource = when (inputs) { is MediaViewerEntryPoint.Params.Avatar -> - SingleMediaGalleryDataSource.createFrom(inputs) + SingleMediaGalleryDataSource.createFrom(inputs, contentValidationCache) is MediaViewerEntryPoint.Params.EventGallery -> GalleryMediaGalleryDataSource.createFrom( eventId = inputs.eventId, galleryItems = inputs.galleryItems, galleryInfo = inputs.galleryInfo, + contentValidationState = inputs.eventId?.let { contentValidationCache[it] } ?: NoopContentValidationState(), ) is MediaViewerEntryPoint.Params.RoomMedia -> { val eventId = inputs.eventId @@ -103,7 +109,7 @@ class MediaViewerNode( } else { focusedTimelineMediaGalleryDataSourceFactory.createFor( eventId = eventId, - mediaItem = inputs.toMediaItem(), + mediaItem = inputs.toMediaItem(contentValidationCache[eventId]), onlyPinnedEvents = false, ) } @@ -111,7 +117,7 @@ class MediaViewerNode( Timeline.Mode.PinnedEvents -> { focusedTimelineMediaGalleryDataSourceFactory.createFor( eventId = eventId, - mediaItem = inputs.toMediaItem(), + mediaItem = inputs.toMediaItem(contentValidationCache[eventId]), onlyPinnedEvents = true, ) } @@ -142,6 +148,8 @@ class MediaViewerNode( localMediaFactory = localMediaFactory, systemClock = systemClock, pagerKeysHandler = pagerKeysHandler, + contentScannerService = contentScannerService, + contentValidationCache = contentValidationCache, ) ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt index eaea61b5a5..60c14f562e 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt @@ -42,7 +42,9 @@ import io.element.android.libraries.mediaviewer.impl.model.MediaPermissions import io.element.android.libraries.mediaviewer.impl.model.mediaPermissions import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import io.element.android.libraries.androidutils.R as UtilsR @@ -70,6 +72,7 @@ class MediaViewerPresenter( private val eventId = inputs.eventId() private val mediaSource = inputs.mediaSource() + @OptIn(ExperimentalCoroutinesApi::class) @Composable override fun present(): MediaViewerState { val coroutineScope = rememberCoroutineScope() @@ -93,7 +96,7 @@ class MediaViewerPresenter( // Restore index based on the eventId after the initial items have been loaded currentIndex.intValue = dataSource.findEventIndex(eventId, mediaSource) ?: 0 } - value = new + value = new.toImmutableList() } } @@ -191,6 +194,7 @@ class MediaViewerPresenter( is MediaViewerEvent.LoadMore -> coroutineScope.launch { dataSource.loadMore(event.direction) } + is MediaViewerEvent.ValidateMedia -> dataSource.validateMedia(event.eventId, event.mediaSource) } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt index a51d3c44e9..03803708af 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState @@ -51,6 +52,7 @@ sealed interface MediaViewerPageData { val mediaSource: MediaSource, val thumbnailSource: MediaSource?, val downloadedMedia: State>, + val validationState: ContentValidationState, override val pagerKey: Long, ) : MediaViewerPageData } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt index 8cc14d6445..440d366c30 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt @@ -17,6 +17,9 @@ import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.aPdfMediaInfo import io.element.android.libraries.mediaviewer.api.aTxtMediaInfo @@ -275,6 +278,34 @@ open class MediaViewerStateProvider : PreviewParameterProvider ) ) }, + anImageMediaInfo( + senderName = "Frank", + dateSent = "26 NOV, 2024", + ).let { + aMediaViewerState( + listOf( + aMediaViewerPageData( + downloadedMedia = AsyncData.Uninitialized, + validationState = NoopContentValidationState(ContentValidationValue.Loading), + mediaInfo = it, + ) + ) + ) + }, + anImageMediaInfo( + senderName = "Frank", + dateSent = "26 NOV, 2024", + ).let { + aMediaViewerState( + listOf( + aMediaViewerPageData( + downloadedMedia = AsyncData.Uninitialized, + validationState = NoopContentValidationState(ContentValidationValue.Invalid), + mediaInfo = it, + ) + ) + ) + }, ) } @@ -293,6 +324,7 @@ fun aMediaViewerPageData( downloadedMedia: AsyncData = AsyncData.Uninitialized, mediaInfo: MediaInfo = anImageMediaInfo(), mediaSource: MediaSource = MediaSource(""), + validationState: ContentValidationState = NoopContentValidationState(), ): MediaViewerPageData.MediaViewerData = MediaViewerPageData.MediaViewerData( eventId = null, mediaInfo = mediaInfo, @@ -300,6 +332,7 @@ fun aMediaViewerPageData( thumbnailSource = null, downloadedMedia = mutableStateOf(downloadedMedia), pagerKey = 0L, + validationState = validationState, ) fun aMediaViewerState( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt index c981132b48..c05bcb6bf6 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -12,11 +12,14 @@ package io.element.android.libraries.mediaviewer.impl.viewer import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.spring import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -73,7 +76,10 @@ import io.element.android.libraries.androidutils.text.safeLinkify import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.async.AsyncFailure +import io.element.android.libraries.designsystem.components.async.AsyncIndicator import io.element.android.libraries.designsystem.components.async.AsyncLoading import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.RetryDialog @@ -176,7 +182,7 @@ fun MediaViewerView( } }, snackbarHost = { SnackbarHost(snackbarHostState) }, - ) { + ) { padding -> val pagerState = rememberPagerState(state.currentIndex, 0f) { state.listData.size } @@ -211,17 +217,33 @@ fun MediaViewerView( ) } is MediaViewerPageData.MediaViewerData -> { + val eventId = dataForPage.eventId + + val isMediaValid = dataForPage.validationState.getCurrentMediaState(dataForPage.mediaSource.safeUrl).isValid() + + // Check if the media to display is valid or dangerous + LaunchedEffect(eventId) { + if (eventId != null) { + state.eventSink(MediaViewerEvent.ValidateMedia(eventId, dataForPage.mediaSource)) + } + } + var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) } - Box( - modifier = Modifier - .onVisibilityChanged(minDurationMs = 200L) { isVisible -> + val loadMediaOnVisibilityChangedModifier = remember(isMediaValid) { + if (isMediaValid == true) { + Modifier.onVisibilityChanged(minDurationMs = 200L) { isVisible -> if (isVisible) { state.eventSink(MediaViewerEvent.LoadMedia(dataForPage)) } else { state.eventSink(MediaViewerEvent.CancelLoadingMedia(dataForPage)) } } - .fillMaxSize() + } else { + Modifier + } + } + Box( + modifier = loadMediaOnVisibilityChangedModifier.fillMaxSize() ) { val isDisplayed = remember(pagerState.settledPage) { // This 'item provider' lambda will be called when the data source changes with an outdated `settlePage` value @@ -232,6 +254,7 @@ fun MediaViewerView( MediaViewerPage( isDisplayed = isDisplayed, showOverlay = showOverlay, + containerPadding = padding, bottomPaddingInPixels = (bottomPaddingInPixels - navigationBarPadding).coerceAtLeast(0), data = dataForPage, textFileViewer = textFileViewer, @@ -333,6 +356,7 @@ private fun MediaViewerPage( isDisplayed: Boolean, showOverlay: Boolean, bottomPaddingInPixels: Int, + containerPadding: PaddingValues, data: MediaViewerPageData.MediaViewerData, textFileViewer: TextFileViewer, isUserSelected: Boolean, @@ -365,6 +389,25 @@ private fun MediaViewerPage( .fillMaxSize() .navigationBarsPadding() ) { + AnimatedVisibility( + visible = data.validationState.getCurrentMediaState(data.mediaSource.safeUrl).isLoading(), + modifier = Modifier.padding(top = containerPadding.calculateTopPadding()).align(Alignment.TopCenter), + enter = fadeIn(spring(stiffness = 500F)), + exit = fadeOut(spring(stiffness = 500F)), + ) { + AsyncIndicator.Loading( + text = stringResource(CommonStrings.content_scanner_scanning), + ) + } + + if (showProgress) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + ) + } + Box(contentAlignment = Alignment.Center) { val zoomableState = rememberZoomableState( zoomSpec = ZoomSpec(maxZoomFactor = 4f, overzoomEffect = OverzoomEffect.NoLimits) @@ -380,29 +423,44 @@ private fun MediaViewerPage( } } - LocalMediaView( - modifier = Modifier.fillMaxSize(), - isDisplayed = isDisplayed, - bottomPaddingInPixels = bottomPaddingInPixels, - localMediaViewState = localMediaViewState, - localMedia = downloadedMedia.dataOrNull(), - mediaInfo = data.mediaInfo, - textFileViewer = textFileViewer, - onClick = { - if (playableState is PlayableState.NotPlayable) { - currentOnShowOverlayChange(!currentShowOverlay) - } - }, - onOpenWith = onOpenWith, - isUserSelected = isUserSelected, - audioFocus = audioFocus, - forPreview = false, - ) - if (showThumbnail) { - ThumbnailView( + val isMediaValid = data.validationState.getCurrentMediaState(data.mediaSource.safeUrl).isValid() + + if (isMediaValid == false) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + ) { + IconTitleSubtitleMolecule( + iconStyle = BigIcon.Style.AlertSolid, + title = "The file is not safe", + subTitle = "Preview and download have been disabled.", + ) + } + } else { + LocalMediaView( + modifier = Modifier.fillMaxSize(), + isDisplayed = isDisplayed, + bottomPaddingInPixels = bottomPaddingInPixels, + localMediaViewState = localMediaViewState, + localMedia = downloadedMedia.dataOrNull(), mediaInfo = data.mediaInfo, - thumbnailSource = data.thumbnailSource, + textFileViewer = textFileViewer, + onClick = { + if (playableState is PlayableState.NotPlayable) { + currentOnShowOverlayChange(!currentShowOverlay) + } + }, + onOpenWith = onOpenWith, + isUserSelected = isUserSelected, + audioFocus = audioFocus, + forPreview = false, ) + if (showThumbnail) { + ThumbnailView( + mediaInfo = data.mediaInfo, + thumbnailSource = data.thumbnailSource, + ) + } } if (showError) { ErrorView( @@ -412,13 +470,6 @@ private fun MediaViewerPage( ) } } - if (showProgress) { - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .height(2.dp) - ) - } } } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt index f347e8181f..1f21252d9f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSource.kt @@ -16,6 +16,9 @@ import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.EventContentValidationCache import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.impl.datasource.MediaGalleryDataSource @@ -27,6 +30,7 @@ import kotlinx.coroutines.flow.flowOf class SingleMediaGalleryDataSource( private val data: GroupedMediaItems, + private val contentValidationCache: EventContentValidationCache, ) : MediaGalleryDataSource { override fun start(coroutineScope: CoroutineScope) = Unit override fun groupedMediaItemsFlow() = flowOf(AsyncData.Success(data)) @@ -38,7 +42,10 @@ class SingleMediaGalleryDataSource( override suspend fun deleteItem(eventId: EventId) = Unit companion object { - fun createFrom(params: MediaViewerEntryPoint.Params.Avatar) = SingleMediaGalleryDataSource( + fun createFrom( + params: MediaViewerEntryPoint.Params.Avatar, + contentValidationCache: EventContentValidationCache, + ) = SingleMediaGalleryDataSource( data = GroupedMediaItems( // Always use imageAndVideoItems, in Single mode, this is the data that will be used imageAndVideoItems = persistentListOf( @@ -64,15 +71,19 @@ class SingleMediaGalleryDataSource( mediaSource = params.mediaSource, thumbnailSource = params.thumbnailSource, blurHash = params.blurHash, + validationState = DefaultContentValidationState(), ) ), fileItems = persistentListOf(), - ) + ), + contentValidationCache = contentValidationCache, ) } } -fun MediaViewerEntryPoint.Params.RoomMedia.toMediaItem() = when { +fun MediaViewerEntryPoint.Params.RoomMedia.toMediaItem( + contentValidationState: ContentValidationState, +) = when { mediaInfo.mimeType.isMimeTypeImage() -> { MediaItem.Image( id = UniqueId("dummy"), @@ -80,7 +91,8 @@ fun MediaViewerEntryPoint.Params.RoomMedia.toMediaItem() = when { mediaInfo = mediaInfo, mediaSource = mediaSource, thumbnailSource = thumbnailSource, - blurHash = blurHash, + blurHash = null, + validationState = contentValidationState, ) } mediaInfo.mimeType.isMimeTypeVideo() -> { @@ -90,7 +102,8 @@ fun MediaViewerEntryPoint.Params.RoomMedia.toMediaItem() = when { mediaInfo = mediaInfo, mediaSource = mediaSource, thumbnailSource = thumbnailSource, - blurHash = blurHash, + blurHash = null, + validationState = contentValidationState, ) } mediaInfo.mimeType.isMimeTypeAudio() -> { @@ -100,6 +113,7 @@ fun MediaViewerEntryPoint.Params.RoomMedia.toMediaItem() = when { eventId = eventId, mediaInfo = mediaInfo, mediaSource = mediaSource, + validationState = contentValidationState, ) } else { MediaItem.Voice( @@ -107,6 +121,7 @@ fun MediaViewerEntryPoint.Params.RoomMedia.toMediaItem() = when { eventId = eventId, mediaInfo = mediaInfo, mediaSource = mediaSource, + validationState = contentValidationState, ) } } @@ -116,6 +131,7 @@ fun MediaViewerEntryPoint.Params.RoomMedia.toMediaItem() = when { eventId = eventId, mediaInfo = mediaInfo, mediaSource = mediaSource, + validationState = contentValidationState, ) } } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt index 0c2e36903c..1691ed9995 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPointTest.kt @@ -12,11 +12,13 @@ import android.net.Uri import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.bumble.appyx.core.modality.BuildContext import com.google.common.truth.Truth.assertThat +import io.element.android.features.contentscanner.api.ContentScannerService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopEventContentValidationCache import io.element.android.libraries.mediaplayer.test.FakeAudioFocus import io.element.android.libraries.mediaviewer.api.AvatarInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint @@ -71,6 +73,8 @@ class DefaultMediaViewerEntryPointTest { audioFocus = FakeAudioFocus(), sessionId = A_SESSION_ID, enterpriseService = FakeEnterpriseService(), + contentValidationCache = NoopEventContentValidationCache(), + contentScannerService = ContentScannerService { _, _, _ -> }, ) } val callback = object : MediaViewerEntryPoint.Callback { @@ -118,6 +122,8 @@ class DefaultMediaViewerEntryPointTest { audioFocus = FakeAudioFocus(), sessionId = A_SESSION_ID, enterpriseService = FakeEnterpriseService(), + contentValidationCache = NoopEventContentValidationCache(), + contentScannerService = ContentScannerService { _, _, _ -> }, ) } val callback = object : MediaViewerEntryPoint.Callback { diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactoryTest.kt index 7147e74e72..d00f5a3eb2 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/EventItemFactoryTest.kt @@ -52,6 +52,8 @@ import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageCo import io.element.android.libraries.matrix.test.timeline.aStickerContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopEventContentValidationCache import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation @@ -174,6 +176,7 @@ class EventItemFactoryTest { duration = null, ), mediaSource = MediaSource(""), + validationState = noopValidationState, ) ) } @@ -227,6 +230,7 @@ class EventItemFactoryTest { mediaSource = MediaSource(""), thumbnailSource = null, blurHash = null, + validationState = noopValidationState, ) ) } @@ -274,6 +278,7 @@ class EventItemFactoryTest { duration = null, ), mediaSource = MediaSource(""), + validationState = noopValidationState, ) ) } @@ -328,6 +333,7 @@ class EventItemFactoryTest { mediaSource = MediaSource(""), thumbnailSource = null, blurHash = null, + validationState = noopValidationState, ) ) } @@ -379,6 +385,7 @@ class EventItemFactoryTest { duration = "7:36", ), mediaSource = MediaSource(""), + validationState = noopValidationState, ) ) } @@ -440,6 +447,7 @@ class EventItemFactoryTest { mediaSource = MediaSource("image_url"), thumbnailSource = MediaSource("thumbnail_url"), blurHash = null, + validationState = noopValidationState, ) ) } @@ -502,6 +510,7 @@ class EventItemFactoryTest { mediaSource = MediaSource("video_url"), thumbnailSource = MediaSource("thumbnail_url"), blurHash = null, + validationState = noopValidationState, ) ) } @@ -557,6 +566,7 @@ class EventItemFactoryTest { duration = null, ), mediaSource = MediaSource("audio_url"), + validationState = noopValidationState, ) ) } @@ -613,6 +623,7 @@ class EventItemFactoryTest { duration = null, ), mediaSource = MediaSource("file_url"), + validationState = noopValidationState, ) ) } @@ -749,13 +760,16 @@ class EventItemFactoryTest { mediaSource = MediaSource(""), thumbnailSource = null, blurHash = null, + validationState = noopValidationState, ) ) } } +private val noopValidationState = NoopContentValidationState() private fun createEventItemFactory() = EventItemFactory( fileSizeFormatter = FakeFileSizeFormatter(), fileExtensionExtractor = FileExtensionExtractorWithoutValidation(), dateFormatter = FakeDateFormatter(), + contentValidationCache = NoopEventContentValidationCache(noopValidationState), ) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt index a5151f3b68..81bb23d73c 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/datasource/TimelineMediaGalleryDataSourceTest.kt @@ -34,6 +34,8 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopEventContentValidationCache import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems import io.element.android.libraries.mediaviewer.impl.model.MediaItem @@ -250,6 +252,7 @@ class TimelineMediaGalleryDataSourceTest { mediaSource = MediaSource("url"), thumbnailSource = MediaSource("url_thumbnail"), blurHash = A_BLUR_HASH, + validationState = noopValidationState, ) ), fileItems = persistentListOf() @@ -260,6 +263,8 @@ class TimelineMediaGalleryDataSourceTest { } } +private val noopValidationState = NoopContentValidationState() + internal fun TestScope.createTimelineMediaGalleryDataSource( room: JoinedRoom = FakeJoinedRoom( liveTimeline = FakeTimeline(), @@ -282,5 +287,6 @@ fun TestScope.createTimelineMediaItemsFactory() = TimelineMediaItemsFactory( fileSizeFormatter = FakeFileSizeFormatter(), fileExtensionExtractor = FileExtensionExtractorWithoutValidation(), dateFormatter = FakeDateFormatter(), - ), + contentValidationCache = NoopEventContentValidationCache(noopValidationState), + ) ) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt index 3ab95f2703..1ce9b6ff19 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt @@ -534,6 +534,7 @@ class MediaGalleryPresenterTest { mediaLoader = matrixMediaLoader, localMediaActions = localMediaActions, snackbarDispatcher = snackbarDispatcher, + contentScannerService = { _, _, _ -> }, ) } } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSourceTest.kt index 670dfd6242..38f59f48cc 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/GalleryMediaGalleryDataSourceTest.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState import io.element.android.libraries.mediaviewer.api.GalleryInfo import io.element.android.libraries.mediaviewer.api.GalleryItemData import io.element.android.libraries.mediaviewer.api.MediaInfo @@ -95,6 +96,7 @@ class GalleryMediaGalleryDataSourceTest { ) ), galleryInfo = aGalleryInfo(), + contentValidationState = noopValidationState, ) val data = (result.getLastData() as AsyncData.Success).data assertThat(data.fileItems).isEmpty() @@ -106,6 +108,7 @@ class GalleryMediaGalleryDataSourceTest { mediaSource = MediaSource("image_url"), thumbnailSource = MediaSource("thumbnail_url"), blurHash = null, + validationState = noopValidationState, ) ) } @@ -125,6 +128,7 @@ class GalleryMediaGalleryDataSourceTest { ) ), galleryInfo = aGalleryInfo(), + contentValidationState = noopValidationState, ) val data = (result.getLastData() as AsyncData.Success).data assertThat(data.fileItems).isEmpty() @@ -136,6 +140,7 @@ class GalleryMediaGalleryDataSourceTest { mediaSource = MediaSource("video_url"), thumbnailSource = MediaSource("thumbnail_url"), blurHash = null, + validationState = noopValidationState, ) ) } @@ -155,6 +160,7 @@ class GalleryMediaGalleryDataSourceTest { ) ), galleryInfo = aGalleryInfo(), + contentValidationState = noopValidationState, ) val data = (result.getLastData() as AsyncData.Success).data assertThat(data.fileItems).isEmpty() @@ -164,6 +170,7 @@ class GalleryMediaGalleryDataSourceTest { eventId = AN_EVENT_ID, mediaInfo = expectedMediaInfo("audio.mp3", MimeTypes.Mp3), mediaSource = MediaSource("audio_url"), + validationState = noopValidationState, ) ) } @@ -183,6 +190,7 @@ class GalleryMediaGalleryDataSourceTest { ) ), galleryInfo = aGalleryInfo(), + contentValidationState = noopValidationState, ) val data = (result.getLastData() as AsyncData.Success).data assertThat(data.fileItems).isEmpty() @@ -192,6 +200,7 @@ class GalleryMediaGalleryDataSourceTest { eventId = AN_EVENT_ID, mediaInfo = expectedMediaInfo("document.pdf", MimeTypes.Pdf), mediaSource = MediaSource("file_url"), + validationState = noopValidationState, ) ) } @@ -211,6 +220,7 @@ class GalleryMediaGalleryDataSourceTest { ) ), galleryInfo = aGalleryInfo(), + contentValidationState = noopValidationState, ) val data = (result.getLastData() as AsyncData.Success).data val item = data.imageAndVideoItems.single() as MediaItem.Image @@ -249,6 +259,7 @@ class GalleryMediaGalleryDataSourceTest { ), ), galleryInfo = aGalleryInfo(), + contentValidationState = noopValidationState, ) val data = (result.getLastData() as AsyncData.Success).data assertThat(data.fileItems).isEmpty() @@ -258,6 +269,8 @@ class GalleryMediaGalleryDataSourceTest { assertThat((data.imageAndVideoItems[2] as MediaItem.Video).id).isEqualTo(UniqueId("${AN_EVENT_ID.value}_2")) } + private val noopValidationState = NoopContentValidationState() + private fun aGalleryInfo() = GalleryInfo( caption = "A caption", formattedCaption = null, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt index d7ba547ee4..95052e29a0 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt @@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopEventContentValidationCache import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint.MediaViewerMode import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.impl.datasource.FakeMediaGalleryDataSource @@ -290,5 +291,7 @@ class MediaViewerDataSourceTest { localMediaFactory = localMediaFactory, systemClock = FakeSystemClock(), pagerKeysHandler = PagerKeysHandler(), + contentScannerService = { _, _, _ -> }, + contentValidationCache = NoopEventContentValidationCache(), ) } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt index b25b8577ee..0ca6534a6c 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenterTest.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopEventContentValidationCache import io.element.android.libraries.mediaviewer.api.AvatarInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.anApkMediaInfo @@ -969,6 +970,8 @@ internal fun TestScope.createMediaViewerPresenter( localMediaFactory = localMediaFactory, systemClock = FakeSystemClock(), pagerKeysHandler = PagerKeysHandler(), + contentScannerService = { _, _, _ -> }, + contentValidationCache = NoopEventContentValidationCache(), ), room = room, localMediaActions = localMediaActions, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt index ad44d1b372..ebd722f25c 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/SingleMediaGalleryDataSourceTest.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.media.aMediaSource +import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopEventContentValidationCache import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo @@ -36,22 +37,24 @@ class SingleMediaGalleryDataSourceTest { @get:Rule val warmUpRule = WarmUpRule() + private val contentValidationCache = NoopEventContentValidationCache() + @Test fun `function start is no op`() = runTest { - val sut = SingleMediaGalleryDataSource(aGroupedMediaItems()) + val sut = SingleMediaGalleryDataSource(aGroupedMediaItems(), contentValidationCache) sut.start(backgroundScope) } @Test fun `function loadMore is no op`() = runTest { - val sut = SingleMediaGalleryDataSource(aGroupedMediaItems()) + val sut = SingleMediaGalleryDataSource(aGroupedMediaItems(), contentValidationCache) sut.loadMore(Timeline.PaginationDirection.BACKWARDS) sut.loadMore(Timeline.PaginationDirection.FORWARDS) } @Test fun `function deleteItem is no op`() = runTest { - val sut = SingleMediaGalleryDataSource(aGroupedMediaItems()) + val sut = SingleMediaGalleryDataSource(aGroupedMediaItems(), contentValidationCache) sut.deleteItem(AN_EVENT_ID) } @@ -61,7 +64,7 @@ class SingleMediaGalleryDataSourceTest { imageAndVideoItems = listOf(aMediaItemImage()), fileItems = listOf(aMediaItemFile()), ) - val sut = SingleMediaGalleryDataSource(data) + val sut = SingleMediaGalleryDataSource(data, contentValidationCache) assertThat(sut.getLastData()).isEqualTo(AsyncData.Success(data)) } @@ -71,7 +74,7 @@ class SingleMediaGalleryDataSourceTest { imageAndVideoItems = listOf(aMediaItemImage()), fileItems = listOf(aMediaItemFile()), ) - val sut = SingleMediaGalleryDataSource(data) + val sut = SingleMediaGalleryDataSource(data, contentValidationCache) sut.groupedMediaItemsFlow().test { assertThat(awaitItem()).isEqualTo(AsyncData.Success(data)) awaitComplete() @@ -90,6 +93,7 @@ class SingleMediaGalleryDataSourceTest { mediaSource = params.mediaSource, thumbnailSource = params.thumbnailSource, blurHash = null, + validationState = contentValidationCache[AN_EVENT_ID], ) } ) @@ -107,6 +111,7 @@ class SingleMediaGalleryDataSourceTest { mediaSource = params.mediaSource, thumbnailSource = params.thumbnailSource, blurHash = null, + validationState = contentValidationCache[AN_EVENT_ID], ) } ) @@ -122,6 +127,7 @@ class SingleMediaGalleryDataSourceTest { eventId = params.eventId, mediaInfo = params.mediaInfo, mediaSource = params.mediaSource, + validationState = contentValidationCache[AN_EVENT_ID], ) } ) @@ -140,6 +146,7 @@ class SingleMediaGalleryDataSourceTest { eventId = params.eventId, mediaInfo = params.mediaInfo, mediaSource = params.mediaSource, + validationState = contentValidationCache[AN_EVENT_ID], ) } ) @@ -155,6 +162,7 @@ class SingleMediaGalleryDataSourceTest { eventId = params.eventId, mediaInfo = params.mediaInfo, mediaSource = params.mediaSource, + validationState = contentValidationCache[AN_EVENT_ID], ) } ) @@ -165,7 +173,7 @@ class SingleMediaGalleryDataSourceTest { expectedResult: (MediaViewerEntryPoint.Params.RoomMedia) -> MediaItem, ) { val params = aMediaViewerEntryPointParams(mediaInfo) - val result = params.toMediaItem() + val result = params.toMediaItem(contentValidationCache[AN_EVENT_ID],) assertThat(result).isEqualTo(expectedResult(params)) } 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 d9f1bfbc09..b74ae6d0fe 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:de447e1f21d51bd3e21a0fe4de6817c375a408beb8225a6ad5deb85025801e4f -size 8293 +oid sha256:fbcb3345c0486059b8a446e0a9b904345b899e398d07f3363449e577d2c0e9fb +size 30369 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 ca71909e7f..8d6179571f 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:f6c7e6ecb02d33e3849ebd2624dfd1e994d77b9f7f43e7ffe9afcf33e8436d44 -size 13041 +oid sha256:d149cbbffd2698eba03f4d89de3af98fb1ad7bc37849598b95dd3f02e66965a8 +size 43742 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 037395a7f8..d14753f755 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:75f5d30bc99d5b09a69b77d544aaafaf665b1c377405f7ccb04092ac0da0a114 -size 35334 +oid sha256:f1abb765bc08c172adefb0ebd4c78d65b5b52791f54c15d72257b7b5a7192f0e +size 79970 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 93cbbae2f8..8ead5cf497 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:7b3105694519d42956dc164a04b7642dfff4a303d7b96cfd2967ea3ad1bc19cd -size 8071 +oid sha256:63449813a8ca880d291113834561224e2547546580cb10a1c4dd7e35a142c638 +size 29089 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 6c89e912be..a08b41cdef 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:8fd9f5dfc87d0a6e65e9c10c67f4359abde2ce85265770f8a47e3329f2f64a2d -size 12443 +oid sha256:a5efe93f94bd3f0c68adaa21043cbbf02ef1e6aebbbdd137e8e2a8e99f2f6193 +size 41850 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 1fb003e7b3..72b4781c79 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:97f49b6969c5a852624558077f9fc9b0add39a12e76e744a385c85785d4489ec -size 34405 +oid sha256:7c488147fde94e3c74660141125279b12435b9f037359256d70555afcd239a18 +size 77333 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png index ca864249bd..8681053839 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31283735c33057ede4d2ba20024b00ae4a4f45510cfbb47c21f980f7151c3858 -size 8931 +oid sha256:70aa1b155531b531ae7f7e7ac1a5123fd065a3c9080c6319435f63f8c315c4e4 +size 31421 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png index e4eb34aaac..b6eda3a2ad 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a2decb05c73655390dd2cfb3d88d209b17ded46d5d6c40cc8672e5781b48348 -size 13334 +oid sha256:a5e462fa9302567a3ac90a4493e71ccb08ba8b924af686b251791c24c8b8553a +size 44108 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png index d2199b862f..f377ff83a8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21803c66fce41f53cd7796321fb9f3fc774e0c4fc99cd3683a3a8b22e00e2615 -size 35966 +oid sha256:5ecc879f8b304ffd4b95f54d1aeda7208238352af1570be23bc9d8de4fe84677 +size 80846 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png index c9a00ed743..1314bb6c3d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11300a4c85f2e8daa06ddb4a3f8d6f72432dc581dca279366b1229450d3b973c -size 8634 +oid sha256:447a27ce5bcd769a8efda39c39ef3494eaf648cbd0fa4939b6089e58b3f31ea3 +size 30107 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png index e1090265d0..f867981bb1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c8a1759a613527cfe5912f6e811121e29611ed05b8a8cecb95391ffb1bc5be2 -size 12710 +oid sha256:3946799c8c3faba3bfef028f6adba46919bda464b73d83d3dcee95383fec491b +size 42288 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png index e73cf16f0c..914059d9f1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_FileItemView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c53798a077d020700ae58aa93aa507dd7ce88984c48aaabf11ba863e8b1773ed -size 34944 +oid sha256:99a5e5fa3f4e014fd1a2b127646e88d848802d24f3150c45b1fb8e4b405d45d1 +size 78217 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 index ecb8160d12..0394b633bb 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ef97e6e11a66c49ed5ba1863567f24097fe8121ea34f7de5fc5cdda3f0c7a0b -size 9279 +oid sha256:42a0dbb630480cda58746522b69f6368c9cc7295f98a865a3caa4535a8f024f3 +size 32081 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 index 2ee1097de9..fbc23c84c8 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49bec9adfef9cf1637c722f5c96e0fec677c21a4ea3f89fd4555a9c894090be0 -size 9833 +oid sha256:d325f7de3a1c8517e7a924de415ab8da043ae3b44eec5e46c5150b1f58f01a50 +size 32799 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 index 793b579aab..8756055e70 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a010f16ec1221a43ba2bd5266da05f3de48d354b98dfc087974ff605d32d095d -size 9576 +oid sha256:ff893a90f760818bd9c4f5befabf67c90d2ecc30ce26e1f613420ec32285c588 +size 32630 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 index 08a42124fa..23d8c4a18e 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c91316c96e75ba29e3d1bae38cb8b0abf255d7316ff3a616fdbddafc550c744f -size 9715 +oid sha256:458bca1df07c79811b4deb874bb71bb822ae21cf85eb76ecf92c69f8ea10e84d +size 32949 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 index 7250c6af6e..1272ba5775 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f8945f66443988d9b36084868241079e22e61605f21cad055d8e21e8e063c0c -size 9819 +oid sha256:c107c8d928bb50527a24032697dee9427dd1dde5987c0c536c52163ce3b8e2a6 +size 33164 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 index f6828af0c8..8f214b76c7 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cc4cb7aca28fba19e6e5d9398ca7d26c9c361298137552789024f84a78a7f70 -size 8713 +oid sha256:0cd6406db1a8309e2f524d4de88ceed6062ac0902622d16fa73d6d8ca64f780e +size 30306 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 index 2639fb33e1..43098d2d33 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48a1a38461c9d2f0bb66878b977673531808eafc0aba53f17beff779a575707e -size 9337 +oid sha256:8b63323a128da29de0ebee9104ce6473136e5e3e8a11394c61116c683c3d51a7 +size 31109 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 index a68f3fd1af..239aea7dc1 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98990600b7b05b8fe64404ef1f463bf5be99f48b90b6d6aab64f799d5b03f737 -size 9095 +oid sha256:8008dfe4f3a135203766b8107751960f159a1bb3350e1d43ede376075db8d61d +size 30978 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 index de56ab0f99..4b9fe06e2c 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c662180e91e10974f7709364a07fa54194df0061425565e08644e731f5922dfa -size 9196 +oid sha256:f642dd641bf49c51d09fcbbc6a644cf280efaf2e88d3c8f4f85ee51b37eeac2a +size 31190 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 index fc138d7153..165d682461 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c08ae126aab643824bd431aa4449a855e382e65840ebd980d4849e19ed17152 -size 9310 +oid sha256:e383090f2f0f06d902b220e05434045ebdd5be781d8f076e21bdc434a83c5bde +size 31393 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 index eb40570d60..6815b035e0 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a3c0e76d07f250735fd75061fb076732180b254d8e8bc145887e0d307272de0 -size 9340 +oid sha256:cf406dbc78412e6c4e457dfbcbabf05248f2578e938016c4a5a6b5cccf1b0c29 +size 32157 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 index 508eb5b48e..a478fb4e7d 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08357309dca3a8a3cd8a38f0928522eeefd3a0cdd91990fcc590929e187b162a -size 11676 +oid sha256:2506e19dfcd975a69fa533973ead95ea7923dbc67545553bad0b09ca87b3f6e9 +size 40834 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 index 3f7c6715b7..f62505f562 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d03f232373aa8cb3f0f6fceeecbd4b1ef86f8703ac6b887683cbfa5c5e5af4e4 -size 36831 +oid sha256:1a6bc629fad6fdd5e1285cc9ac4484683b111498f989c5fe43f0b037b5619b36 +size 82000 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 index 2cc97641ec..37f6d667a9 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:886dff267c22e2ba300b81f13e3071756483c5938e9d1cfb357f633357f5ff06 -size 7634 +oid sha256:745ea9fca0277511ef197608025135387a985082367e52d52b780ba94498b7aa +size 28992 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 index b83e6823b5..ff3c7036a6 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c11e7b79420af53a8b934b3ad6dbe9c68d8ea5635322389663b55f62160eade -size 8775 +oid sha256:e20e92f594ead4e2d5a0beb2f290077506322ec11eb75f8d16c1374ddb217456 +size 30375 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 index a0ee911211..166d22f9b0 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22f76d8fbeaca8d0b986b4ac17a28c16e5f757e2022466200215fca333f69ede -size 10929 +oid sha256:dd0e274dc81bf5bfa6498922cb3a75721bb98af8fbeaf558d67b127248755543 +size 38543 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 index 6f77d72e3f..cb4cec9dda 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73cc7f1b0f8b9bff2b2e6c9a88b833baaeb9f39923c2113a006399c6a877634a -size 35118 +oid sha256:e54d39cc93385c85ae9c1f8b9f5dd459811c28878f788d0e05562bfb62dfff6d +size 78927 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 index c82f55de11..1474d94884 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4d538dd90a36eefd2f9c8f8f66847cce6bc17f60e503513bf1890e6b5403d12 -size 7244 +oid sha256:4485fe9b2310bde15485f78d7dd556df6453e2567f859ab3268e36d5ec901f4f +size 27453 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png index 68a850744e..d9f30b5a5e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5832d2a257dcf613420352d10c42e058ca9df0afa323f2a6bd91ab6596240e5a -size 528252 +oid sha256:b5bb8fe5d28d0df28c25c633d5e37aa6245d69e754e3ec543d855fd4a64517fb +size 448654 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 be668fb03c..e1fb7f49bf 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:691c6ffd759b01f216128c1f7d59e7cae76e1f1a83c95a8b89e6f2f55dce3b9c -size 33009 +oid sha256:a7f75b9cad16c26fdca283a79b871be4b4c41d8c15b1d4ea96c82dd37015761e +size 48065 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png index c6267965cd..63afe83d3c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5fbccba6548b6101f50f3842310e591507321817b048ec9a7310203dcd868d9 -size 524645 +oid sha256:ece2446eaa8417ce67a32589fa2901a8e2c73b6475b46b1bfb895ee3aacb6937 +size 445037 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 341dc6115f..09dc2830bb 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:e218343a909a15b66c6c00f46c6bbdd783ad1365aa380ed576a9e66c6dc3c09f -size 31646 +oid sha256:a4d6f6ad2d759fd2383eed4d051880851cd3b46a03812e6ce43a372dd4542be8 +size 46007 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en.png index 12f6ba7c36..7d18f051ef 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f10ca2d461e4078e46455e007eca2d1e9c9a20dbb6bc24c681fa5164e5f50efd -size 13531 +oid sha256:d5c4ad66332bc73642009c013cf99b4b67dfe22d34dfe9db5d30e18dbafa2f10 +size 8905 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en.png index 6273b591af..ecef82ecca 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.local.video_MediaVideoView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da386980ce6a45e102727715b5d50f219e39dcb40a513abdfaab9fa725b8ea41 -size 13260 +oid sha256:921ac0d2d3ced6fc6c21c3fa82c49a783bf1c1ecababbac8d07364c5ee5a2573 +size 8727 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png index a737f60d1b..f9fb733b8f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:348a194ad17a0a0dffda47c7387edde99436d543e9bff1726608946e92558830 -size 185553 +oid sha256:be416c0da077b81dbbe3c61c750bcaa3398150ac3444f1f9a365709d2ef647d0 +size 185717 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png index 312b19a389..806a046288 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b23ef4fb29f51e308b74681017fcee257308edbd1e218e4daf5736501c70e0de -size 699637 +oid sha256:06735917160f8f210909922ccf765b806c64586853fad685634a006fe59af974 +size 700189 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_23_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_23_en.png new file mode 100644 index 0000000000..704d3432cf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_23_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47779722cc50bb7181d300e5738d10c32824c87122faef5939c4774e1b6530d8 +size 23583 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_24_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_24_en.png new file mode 100644 index 0000000000..3620fdacf9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_24_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c23fdfa983bc6032756dfb80149f8f39643215eab97ddb8d9c843401262bfc6 +size 20022 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png index 3afa92e837..7510665188 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:515d5a47d530117ff21cb922d1cb2cb81b22d02c083c51bbff69964f92260bbe -size 197095 +oid sha256:d3403da236e7e9e8a28001adcb0f5c188d79bdf83146b69e8094b37e02627b28 +size 197245 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png index 6ac98e0150..698903e181 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerViewLandscape_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2871bd26b6c21d31ca251761eb5a203468406b4d18c13662fa815c53f015b5e1 -size 210297 +oid sha256:3454f97fd3811411ea5cd941b3d1e37a0f5ab98b9ef7475fa2c903c2b6b089d1 +size 210375 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_23_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_23_en.png new file mode 100644 index 0000000000..98cca36417 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_23_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc42702e3ee2b646b4763fef985568f03d3d1ad971fd426328a1d9820045cbb8 +size 24235 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_24_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_24_en.png new file mode 100644 index 0000000000..0193d6c61f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_24_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2d3fd5b70d90864b581d71b064a684f1dfbaa368464c32d259dc418a4ab77f5 +size 20821 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png index 53aafed48f..0f87d86340 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc65f284ec05a8ed7ea496c49c52afae585f09d26986d8582c00385459823393 -size 136782 +oid sha256:9f3f46de75cee217874e2c57b390998136a19d32613ea33a2e303541a4fd165b +size 136770