diff --git a/features/contentscanner/api/src/main/kotlin/io/element/android/features/contentscanner/api/ContentScannerService.kt b/features/contentscanner/api/src/main/kotlin/io/element/android/features/contentscanner/api/ContentScannerService.kt index cc5fca38a0..68f7af28f1 100644 --- a/features/contentscanner/api/src/main/kotlin/io/element/android/features/contentscanner/api/ContentScannerService.kt +++ b/features/contentscanner/api/src/main/kotlin/io/element/android/features/contentscanner/api/ContentScannerService.kt @@ -7,17 +7,16 @@ package io.element.android.features.contentscanner.api -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.scanner.ContentScanner import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState /** - * Service to perform security scans in the contents for a given event and media source. + * Service to perform security scans in the contents for a given media source. * * The default implementation (FOSS) always returns a valid state. * This will only process media when a [ContentScanner] is provided when building the client in Pro. */ fun interface ContentScannerService { - fun scan(eventId: EventId, mediaSources: List, contentValidationState: ContentValidationState) + fun scan(mediaSources: List, contentValidationState: ContentValidationState) } 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 c809179552..8b75a79d2c 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 @@ -9,7 +9,6 @@ package io.element.android.features.contentscanner.impl import io.element.android.features.contentscanner.api.ContentScannerService import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.scanner.ContentScanner @@ -29,7 +28,7 @@ class DefaultContentScannerService( ) : ContentScannerService { private val dispatcher = coroutineDispatchers.io.limitedParallelism(4) - override fun scan(eventId: EventId, mediaSources: List, contentValidationState: ContentValidationState) { + override fun scan(mediaSources: List, contentValidationState: ContentValidationState) { for (mediaSource in mediaSources) { val url = mediaSource.safeUrl val currentState = contentValidationState.getCurrentMediaState(url) diff --git a/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/di/AlwaysValidContentScannerService.kt b/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/di/AlwaysValidContentScannerService.kt index 6170f3e153..cd96433ce4 100644 --- a/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/di/AlwaysValidContentScannerService.kt +++ b/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/di/AlwaysValidContentScannerService.kt @@ -8,7 +8,6 @@ package io.element.android.features.contentscanner.impl.di import io.element.android.features.contentscanner.api.ContentScannerService -import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue @@ -19,7 +18,7 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentVal * This is used when the content scanner feature is not enabled or available. */ class AlwaysValidContentScannerService : ContentScannerService { - override fun scan(eventId: EventId, mediaSources: List, contentValidationState: ContentValidationState) { + override fun scan(mediaSources: List, contentValidationState: ContentValidationState) { // Always return success for the noop implementation for (mediaSource in mediaSources) { val url = mediaSource.safeUrl 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 4bd20ebaf8..ecfdd9715d 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 @@ -11,7 +11,6 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.scanner.ContentScanner -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.test.scanner.FakeContentScanner import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue @@ -39,7 +38,6 @@ class DefaultContentScannerServiceTest { createDefaultContentScannerService( contentScanner = scanner, ).scan( - eventId = AN_EVENT_ID, mediaSources = emptyList(), contentValidationState = contentValidationState, ) @@ -58,7 +56,6 @@ class DefaultContentScannerServiceTest { createDefaultContentScannerService( contentScanner = scanner, ).scan( - eventId = AN_EVENT_ID, mediaSources = listOf(mediaSource), contentValidationState = contentValidationState, ) @@ -87,7 +84,6 @@ class DefaultContentScannerServiceTest { createDefaultContentScannerService( contentScanner = scanner, ).scan( - eventId = AN_EVENT_ID, mediaSources = listOf(mediaSourceA, mediaSourceB), contentValidationState = contentValidationState, ) @@ -113,14 +109,12 @@ class DefaultContentScannerServiceTest { // First scan attempt service.scan( - eventId = AN_EVENT_ID, mediaSources = listOf(mediaSource), contentValidationState = contentValidationState, ) // Immediately after, a new scan attempt is made for the same media source service.scan( - eventId = AN_EVENT_ID, mediaSources = listOf(mediaSource), contentValidationState = contentValidationState, ) @@ -144,7 +138,6 @@ class DefaultContentScannerServiceTest { ) service.scan( - eventId = AN_EVENT_ID, mediaSources = listOf(mediaSource), contentValidationState = contentValidationState, ) @@ -166,7 +159,6 @@ class DefaultContentScannerServiceTest { ) service.scan( - eventId = AN_EVENT_ID, mediaSources = listOf(mediaSource), contentValidationState = contentValidationState, ) @@ -191,7 +183,6 @@ class DefaultContentScannerServiceTest { ) service.scan( - eventId = AN_EVENT_ID, mediaSources = listOf(mediaSource), contentValidationState = contentValidationState, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 80dd21b7b3..18252ee17d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -858,7 +858,7 @@ class MessageComposerPresenter( if (currentComposerMode is MessageComposerMode.Reply) { val mediaSources = currentComposerMode.replyToDetails.content()?.mediaSources() ?: return@launch val contentValidationState = contentValidationCache[currentComposerMode.replyToDetails.eventId()] - contentScannerService.scan(currentComposerMode.replyToDetails.eventId(), mediaSources, contentValidationState) + contentScannerService.scan(mediaSources, contentValidationState) } // When coming from edit, just clear the composer as it'd be weird to reset a volatile draft in this scenario. if (currentComposerMode.isEditing) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 9a33299cd0..7cd0d018d2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -259,7 +259,7 @@ class TimelinePresenter( ) } is TimelineEvent.ValidateMedia -> { - timelineProtectionState.eventSink(TimelineProtectionEvent.ValidateContent(event.eventId, event.mediaSources, event.validationState)) + timelineProtectionState.eventSink(TimelineProtectionEvent.ValidateContent(event.mediaSources, event.validationState)) } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionEvent.kt index 75a676cdb7..72c5eb7a30 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionEvent.kt @@ -15,7 +15,6 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentVal sealed interface TimelineProtectionEvent { data class ShowContent(val eventId: EventId?) : TimelineProtectionEvent data class ValidateContent( - val eventId: EventId, val mediaSources: List, val validationState: ContentValidationState, ) : TimelineProtectionEvent diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt index dc70b63f32..22be4acb2d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenter.kt @@ -59,7 +59,7 @@ class TimelineProtectionPresenter( allowedEvents += setOfNotNull(event.eventId) } is TimelineProtectionEvent.ValidateContent -> { - contentScannerService.scan(event.eventId, event.mediaSources, event.validationState) + contentScannerService.scan(event.mediaSources, event.validationState) } } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt index 0ac8c3be1f..faf0bbe6ed 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterSlashCommandTest.kt @@ -316,7 +316,7 @@ class MessageComposerPresenterSlashCommandTest { notificationConversationService = notificationConversationService, slashCommandService = slashCommandService, featureFlagService = featureFlagService, - contentScannerService = { _, _, _ -> }, + contentScannerService = { _, _ -> }, contentValidationCache = InMemoryEventContentValidationCache(), ).apply { isTesting = true diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index c164a30160..fda8621087 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -1650,7 +1650,7 @@ class MessageComposerPresenterTest : RobolectricTest() { notificationConversationService = notificationConversationService, slashCommandService = slashCommandService, featureFlagService = featureFlagService, - contentScannerService = { _, _, _ -> }, + contentScannerService = { _, _ -> }, contentValidationCache = InMemoryEventContentValidationCache(), ).apply { isTesting = true diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt index 11093bd74f..11142cc9a2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/protection/TimelineProtectionPresenterTest.kt @@ -10,7 +10,6 @@ package io.element.android.features.messages.impl.timeline.protection import com.google.common.truth.Truth.assertThat import io.element.android.features.contentscanner.api.ContentScannerService -import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaPreviewConfig import io.element.android.libraries.matrix.api.media.MediaPreviewService import io.element.android.libraries.matrix.api.media.MediaPreviewValue @@ -104,7 +103,7 @@ class TimelineProtectionPresenterTest { val mediaPreviewConfig = MediaPreviewConfig(mediaPreviewValue = MediaPreviewValue.Private, hideInviteAvatar = false) val mediaPreviewService = FakeMediaPreviewService(mediaPreviewConfigFlow = MutableStateFlow(mediaPreviewConfig)) val room = FakeBaseRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite), roomCoroutineScope = backgroundScope) - val contentScannerService = lambdaRecorder { _: EventId, _: List, state: ContentValidationState -> + val contentScannerService = lambdaRecorder { _: List, state: ContentValidationState -> state.update(url, ContentValidationValue.Valid) } val presenter = createPresenter( @@ -119,7 +118,6 @@ class TimelineProtectionPresenterTest { val initialState = awaitItem() initialState.eventSink( TimelineProtectionEvent.ValidateContent( - eventId = AN_EVENT_ID, mediaSources = listOf(MediaSource(url)), validationState = validationState ) @@ -135,7 +133,7 @@ class TimelineProtectionPresenterTest { private fun createPresenter( room: BaseRoom = FakeBaseRoom(), mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(), - contentScannerService: ContentScannerService = ContentScannerService { _: EventId, _: List, _: ContentValidationState -> }, + contentScannerService: ContentScannerService = ContentScannerService { _: List, _: ContentValidationState -> }, ) = TimelineProtectionPresenter( mediaPreviewService = mediaPreviewService, room = room, 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 bbaf52f5b0..bfc01c4255 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 @@ -189,7 +189,7 @@ fun ContentValidationState.collectOverallState(): State @Composable fun ContentValidationState.collectMediaState(url: String?): State { if (url == null) { - return remember { mutableStateOf(ContentValidationValue.Unknown) } + return remember { mutableStateOf(ContentValidationValue.Valid) } } return produceState(getCurrentMediaState(url), key1 = url) { 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 2add4630e7..e909c5367c 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 @@ -84,9 +84,8 @@ class MediaGalleryPresenter( 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) + if (item is MediaItem.Event && item.eventId() != null) { + coroutineScope.validateMedia(item) } } } @@ -237,7 +236,7 @@ class MediaGalleryPresenter( } } - private fun CoroutineScope.validateMedia(eventId: EventId, mediaItem: MediaItem.Event) { + private fun CoroutineScope.validateMedia(mediaItem: MediaItem.Event) { launch { val thumbnailSource = mediaItem.thumbnailSource() val mediaSource = mediaItem.mediaSource() @@ -248,7 +247,7 @@ class MediaGalleryPresenter( return@launch } - contentScannerService.scan(eventId, listOfNotNull(thumbnailSource, mediaSource).distinct(), contentValidationState) + contentScannerService.scan(listOfNotNull(thumbnailSource, mediaSource).distinct(), contentValidationState) } } 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 8b48955502..e1be702c9c 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 @@ -242,18 +242,15 @@ class MediaViewerDataSource( } } - fun validateMedia( - eventId: EventId, - mediaSource: MediaSource, - ) { + fun validateMedia(mediaSource: MediaSource, thumbnailMediaSource: MediaSource?) { val validationState = mediaValidationState[mediaSource.safeUrl] ?: return - val currentState = validationState.getCurrentMediaState(mediaSource.safeUrl) + val currentState = validationState.getCurrentOverallState() if (currentState.isLoading() || currentState.isValid()) { return } - contentScannerService.scan(eventId, listOf(mediaSource), validationState) + contentScannerService.scan(listOfNotNull(mediaSource, thumbnailMediaSource), validationState) } fun cancelLoadingMedia(data: MediaViewerPageData.MediaViewerData) { 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 21769f81be..8374294075 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 @@ -31,5 +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 + data class ValidateMedia(val mediaSource: MediaSource, val thumbnailMediaSource: MediaSource?) : MediaViewerEvent } 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 60c14f562e..35dc4d97ba 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 @@ -194,7 +194,7 @@ class MediaViewerPresenter( is MediaViewerEvent.LoadMore -> coroutineScope.launch { dataSource.loadMore(event.direction) } - is MediaViewerEvent.ValidateMedia -> dataSource.validateMedia(event.eventId, event.mediaSource) + is MediaViewerEvent.ValidateMedia -> dataSource.validateMedia(event.mediaSource, event.thumbnailMediaSource) } } 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 c05bcb6bf6..1083f26685 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 @@ -95,6 +95,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.matrix.ui.media.contentvalidation.collectMediaState 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 @@ -217,20 +218,24 @@ fun MediaViewerView( ) } is MediaViewerPageData.MediaViewerData -> { - val eventId = dataForPage.eventId - - val isMediaValid = dataForPage.validationState.getCurrentMediaState(dataForPage.mediaSource.safeUrl).isValid() + // We need to check the 2 validation states and can't use the overall state because these might be part of a gallery event + // If that was the case, the overall state would be invalid if any of the sources for any media in the gallery was invalid, e.g.: + // Media 1: mediaSource = valid, thumbnailSource = valid + // Media 2: mediaSource = invalid, thumbnailSource = valid + // The overall state would be invalid, so the whole gallery event and all its medias would be considered invalid. + val mediaValidationState by dataForPage.validationState.collectMediaState(dataForPage.mediaSource.safeUrl) + val thumbnailValidationState by dataForPage.validationState.collectMediaState(dataForPage.thumbnailSource?.safeUrl) // Check if the media to display is valid or dangerous - LaunchedEffect(eventId) { - if (eventId != null) { - state.eventSink(MediaViewerEvent.ValidateMedia(eventId, dataForPage.mediaSource)) - } + LaunchedEffect(Unit) { + state.eventSink( + MediaViewerEvent.ValidateMedia(mediaSource = dataForPage.mediaSource, thumbnailMediaSource = dataForPage.thumbnailSource) + ) } var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) } - val loadMediaOnVisibilityChangedModifier = remember(isMediaValid) { - if (isMediaValid == true) { + val loadMediaOnVisibilityChangedModifier = remember(mediaValidationState) { + if (mediaValidationState.isValid() && thumbnailValidationState.isValid()) { Modifier.onVisibilityChanged(minDurationMs = 200L) { isVisible -> if (isVisible) { state.eventSink(MediaViewerEvent.LoadMedia(dataForPage)) @@ -383,6 +388,8 @@ private fun MediaViewerPage( ) { val downloadedMedia by data.downloadedMedia val showProgress = rememberShowProgress(downloadedMedia) + val mediaValidationState by data.validationState.collectMediaState(data.mediaSource.safeUrl) + val thumbnailValidationState by data.validationState.collectMediaState(data.thumbnailSource?.safeUrl) Box( modifier = Modifier @@ -390,7 +397,7 @@ private fun MediaViewerPage( .navigationBarsPadding() ) { AnimatedVisibility( - visible = data.validationState.getCurrentMediaState(data.mediaSource.safeUrl).isLoading(), + visible = mediaValidationState.isLoading(), modifier = Modifier.padding(top = containerPadding.calculateTopPadding()).align(Alignment.TopCenter), enter = fadeIn(spring(stiffness = 500F)), exit = fadeOut(spring(stiffness = 500F)), @@ -423,20 +430,18 @@ private fun MediaViewerPage( } } - val isMediaValid = data.validationState.getCurrentMediaState(data.mediaSource.safeUrl).isValid() - - if (isMediaValid == false) { + if (mediaValidationState.isInvalid() || thumbnailValidationState.isInvalid()) { 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.", + title = stringResource(CommonStrings.content_scanner_unsafe_title), + subTitle = stringResource(CommonStrings.content_scanner_unsafe_message), ) } - } else { + } else if (mediaValidationState.isValid() && thumbnailValidationState.isValid()) { LocalMediaView( modifier = Modifier.fillMaxSize(), isDisplayed = isDisplayed, @@ -462,6 +467,7 @@ private fun MediaViewerPage( ) } } + if (showError) { ErrorView( errorMessage = stringResource(id = CommonStrings.error_unknown), 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 1691ed9995..3ca702a47f 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 @@ -74,7 +74,7 @@ class DefaultMediaViewerEntryPointTest { sessionId = A_SESSION_ID, enterpriseService = FakeEnterpriseService(), contentValidationCache = NoopEventContentValidationCache(), - contentScannerService = ContentScannerService { _, _, _ -> }, + contentScannerService = ContentScannerService { _, _ -> }, ) } val callback = object : MediaViewerEntryPoint.Callback { @@ -123,7 +123,7 @@ class DefaultMediaViewerEntryPointTest { sessionId = A_SESSION_ID, enterpriseService = FakeEnterpriseService(), contentValidationCache = NoopEventContentValidationCache(), - contentScannerService = ContentScannerService { _, _, _ -> }, + contentScannerService = ContentScannerService { _, _ -> }, ) } val callback = object : MediaViewerEntryPoint.Callback { 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 1ce9b6ff19..a6e7e6b201 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,7 +534,7 @@ class MediaGalleryPresenterTest { mediaLoader = matrixMediaLoader, localMediaActions = localMediaActions, snackbarDispatcher = snackbarDispatcher, - contentScannerService = { _, _, _ -> }, + contentScannerService = { _, _ -> }, ) } } 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 95052e29a0..bc8e3f1a50 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 @@ -291,7 +291,7 @@ class MediaViewerDataSourceTest { localMediaFactory = localMediaFactory, systemClock = FakeSystemClock(), pagerKeysHandler = PagerKeysHandler(), - contentScannerService = { _, _, _ -> }, + 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 0ca6534a6c..be577362bd 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 @@ -970,7 +970,7 @@ internal fun TestScope.createMediaViewerPresenter( localMediaFactory = localMediaFactory, systemClock = FakeSystemClock(), pagerKeysHandler = PagerKeysHandler(), - contentScannerService = { _, _, _ -> }, + contentScannerService = { _, _ -> }, contentValidationCache = NoopEventContentValidationCache(), ), room = room, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt index d2780bb408..ef668849d7 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt @@ -47,6 +47,24 @@ import kotlin.time.Duration.Companion.milliseconds class MediaViewerViewTest : RobolectricTest() { private val mockMediaUrl: Uri = mockk("localMediaUri") + @Test + fun `displaying a media will automatically validate it`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + val pagerData = aMediaViewerPageData(downloadedMedia = AsyncData.Success(aLocalMedia(uri = mockMediaUrl))) + val state = aMediaViewerState( + listData = listOf(pagerData), + eventSink = eventsRecorder + ) + setMediaViewerView( + state = state, + ) + eventsRecorder.assertList( + listOf( + MediaViewerEvent.ValidateMedia(mediaSource = pagerData.mediaSource, thumbnailMediaSource = pagerData.thumbnailSource), + ) + ) + } + @Test fun `clicking on back invokes expected callback`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() @@ -59,6 +77,9 @@ class MediaViewerViewTest : RobolectricTest() { onBackClick = callback, ) + // Remove the `ValidateMedia` event that is emitted when the media is loaded, since we are not testing it here. + eventsRecorder.clear() + // Wait for enough time for the onVisibilityChanged modifier to trigger mainClock.advanceTimeBy(200) @@ -120,6 +141,9 @@ class MediaViewerViewTest : RobolectricTest() { ), ) + // Remove the `ValidateMedia` event that is emitted when the media is loaded, since we are not testing it here. + eventsRecorder.clear() + // Wait for enough time for the onVisibilityChanged modifier to trigger mainClock.advanceTimeBy(200) @@ -157,9 +181,11 @@ class MediaViewerViewTest : RobolectricTest() { eventSink = eventsRecorder ), ) + clickOn(textRes) eventsRecorder.assertList( listOf( + MediaViewerEvent.ValidateMedia(mediaSource = data.mediaSource, thumbnailMediaSource = data.thumbnailSource), MediaViewerEvent.LoadMedia(data), expectedEvent, ) @@ -175,6 +201,10 @@ class MediaViewerViewTest : RobolectricTest() { setMediaViewerView( state = state, ) + + // Remove the `ValidateMedia` event that is emitted when the media is loaded, since we are not testing it here. + eventsRecorder.clear() + // Ensure that the action are visible val resources = activity!!.resources val contentDescription = resources.getString(CommonStrings.action_share) @@ -205,6 +235,10 @@ class MediaViewerViewTest : RobolectricTest() { state = state, onBackClick = callback, ) + + // Remove the `ValidateMedia` event that is emitted when the media is loaded, since we are not testing it here. + eventsRecorder.clear() + val imageContentDescription = activity!!.getString(CommonStrings.common_image) onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) } mainClock.advanceTimeBy(1_000) @@ -229,6 +263,9 @@ class MediaViewerViewTest : RobolectricTest() { ), ) + // Remove the `ValidateMedia` event that is emitted when the media is loaded, since we are not testing it here. + eventsRecorder.clear() + // Wait for enough time for the onVisibilityChanged modifier to trigger mainClock.advanceTimeBy(200) @@ -254,6 +291,9 @@ class MediaViewerViewTest : RobolectricTest() { ), ) + // Remove the `ValidateMedia` event that is emitted when the media is loaded, since we are not testing it here. + eventsRecorder.clear() + // Wait for enough time for the onVisibilityChanged modifier to trigger mainClock.advanceTimeBy(200) 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 index 704d3432cf..96ee2aa0dc 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47779722cc50bb7181d300e5738d10c32824c87122faef5939c4774e1b6530d8 -size 23583 +oid sha256:fa4e4a853c228493b0b2f65bfb903d471756ca669446cd62395fc0afa3ee47e8 +size 11997 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 index 98cca36417..e5ac12f739 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc42702e3ee2b646b4763fef985568f03d3d1ad971fd426328a1d9820045cbb8 -size 24235 +oid sha256:8e85ebc527d837aa3eba64e14b39cf7d7bdc9e08eff6b1c473e5698f879ea0eb +size 12389