mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 01:19:33 +00:00
Fix media viewer for medias not related to events displaying invalid state (#7280)
* When no URL is provided to `ContentValidationState.collectMediaState`, assume it's valid That way we can do: `if (validationState.isValid() && state.collectMediaState(possiblyNullUrl).isValid()) ...` * Remove the `eventId` param from `ContentScannerService.scan` It's not really used, since we then provide a state that can be mutated, and fetching that is what the previous parameter was used for * Make sure to always validate the medias in the viewer, both the actual media and the thumbnail * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
d8b01cf3e7
commit
0442fbd307
+2
-3
@@ -7,17 +7,16 @@
|
|||||||
|
|
||||||
package io.element.android.features.contentscanner.api
|
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.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
|
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.
|
* 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.
|
* This will only process media when a [ContentScanner] is provided when building the client in Pro.
|
||||||
*/
|
*/
|
||||||
fun interface ContentScannerService {
|
fun interface ContentScannerService {
|
||||||
fun scan(eventId: EventId, mediaSources: List<MediaSource>, contentValidationState: ContentValidationState)
|
fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -9,7 +9,6 @@ package io.element.android.features.contentscanner.impl
|
|||||||
|
|
||||||
import io.element.android.features.contentscanner.api.ContentScannerService
|
import io.element.android.features.contentscanner.api.ContentScannerService
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
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.exception.ClientException
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
||||||
@@ -29,7 +28,7 @@ class DefaultContentScannerService(
|
|||||||
) : ContentScannerService {
|
) : ContentScannerService {
|
||||||
private val dispatcher = coroutineDispatchers.io.limitedParallelism(4)
|
private val dispatcher = coroutineDispatchers.io.limitedParallelism(4)
|
||||||
|
|
||||||
override fun scan(eventId: EventId, mediaSources: List<MediaSource>, contentValidationState: ContentValidationState) {
|
override fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState) {
|
||||||
for (mediaSource in mediaSources) {
|
for (mediaSource in mediaSources) {
|
||||||
val url = mediaSource.safeUrl
|
val url = mediaSource.safeUrl
|
||||||
val currentState = contentValidationState.getCurrentMediaState(url)
|
val currentState = contentValidationState.getCurrentMediaState(url)
|
||||||
|
|||||||
+1
-2
@@ -8,7 +8,6 @@
|
|||||||
package io.element.android.features.contentscanner.impl.di
|
package io.element.android.features.contentscanner.impl.di
|
||||||
|
|
||||||
import io.element.android.features.contentscanner.api.ContentScannerService
|
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.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
|
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.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.
|
* This is used when the content scanner feature is not enabled or available.
|
||||||
*/
|
*/
|
||||||
class AlwaysValidContentScannerService : ContentScannerService {
|
class AlwaysValidContentScannerService : ContentScannerService {
|
||||||
override fun scan(eventId: EventId, mediaSources: List<MediaSource>, contentValidationState: ContentValidationState) {
|
override fun scan(mediaSources: List<MediaSource>, contentValidationState: ContentValidationState) {
|
||||||
// Always return success for the noop implementation
|
// Always return success for the noop implementation
|
||||||
for (mediaSource in mediaSources) {
|
for (mediaSource in mediaSources) {
|
||||||
val url = mediaSource.safeUrl
|
val url = mediaSource.safeUrl
|
||||||
|
|||||||
-9
@@ -11,7 +11,6 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.scanner.ContentScanner
|
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.media.aMediaSource
|
||||||
import io.element.android.libraries.matrix.test.scanner.FakeContentScanner
|
import io.element.android.libraries.matrix.test.scanner.FakeContentScanner
|
||||||
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
|
||||||
@@ -39,7 +38,6 @@ class DefaultContentScannerServiceTest {
|
|||||||
createDefaultContentScannerService(
|
createDefaultContentScannerService(
|
||||||
contentScanner = scanner,
|
contentScanner = scanner,
|
||||||
).scan(
|
).scan(
|
||||||
eventId = AN_EVENT_ID,
|
|
||||||
mediaSources = emptyList(),
|
mediaSources = emptyList(),
|
||||||
contentValidationState = contentValidationState,
|
contentValidationState = contentValidationState,
|
||||||
)
|
)
|
||||||
@@ -58,7 +56,6 @@ class DefaultContentScannerServiceTest {
|
|||||||
createDefaultContentScannerService(
|
createDefaultContentScannerService(
|
||||||
contentScanner = scanner,
|
contentScanner = scanner,
|
||||||
).scan(
|
).scan(
|
||||||
eventId = AN_EVENT_ID,
|
|
||||||
mediaSources = listOf(mediaSource),
|
mediaSources = listOf(mediaSource),
|
||||||
contentValidationState = contentValidationState,
|
contentValidationState = contentValidationState,
|
||||||
)
|
)
|
||||||
@@ -87,7 +84,6 @@ class DefaultContentScannerServiceTest {
|
|||||||
createDefaultContentScannerService(
|
createDefaultContentScannerService(
|
||||||
contentScanner = scanner,
|
contentScanner = scanner,
|
||||||
).scan(
|
).scan(
|
||||||
eventId = AN_EVENT_ID,
|
|
||||||
mediaSources = listOf(mediaSourceA, mediaSourceB),
|
mediaSources = listOf(mediaSourceA, mediaSourceB),
|
||||||
contentValidationState = contentValidationState,
|
contentValidationState = contentValidationState,
|
||||||
)
|
)
|
||||||
@@ -113,14 +109,12 @@ class DefaultContentScannerServiceTest {
|
|||||||
|
|
||||||
// First scan attempt
|
// First scan attempt
|
||||||
service.scan(
|
service.scan(
|
||||||
eventId = AN_EVENT_ID,
|
|
||||||
mediaSources = listOf(mediaSource),
|
mediaSources = listOf(mediaSource),
|
||||||
contentValidationState = contentValidationState,
|
contentValidationState = contentValidationState,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Immediately after, a new scan attempt is made for the same media source
|
// Immediately after, a new scan attempt is made for the same media source
|
||||||
service.scan(
|
service.scan(
|
||||||
eventId = AN_EVENT_ID,
|
|
||||||
mediaSources = listOf(mediaSource),
|
mediaSources = listOf(mediaSource),
|
||||||
contentValidationState = contentValidationState,
|
contentValidationState = contentValidationState,
|
||||||
)
|
)
|
||||||
@@ -144,7 +138,6 @@ class DefaultContentScannerServiceTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
service.scan(
|
service.scan(
|
||||||
eventId = AN_EVENT_ID,
|
|
||||||
mediaSources = listOf(mediaSource),
|
mediaSources = listOf(mediaSource),
|
||||||
contentValidationState = contentValidationState,
|
contentValidationState = contentValidationState,
|
||||||
)
|
)
|
||||||
@@ -166,7 +159,6 @@ class DefaultContentScannerServiceTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
service.scan(
|
service.scan(
|
||||||
eventId = AN_EVENT_ID,
|
|
||||||
mediaSources = listOf(mediaSource),
|
mediaSources = listOf(mediaSource),
|
||||||
contentValidationState = contentValidationState,
|
contentValidationState = contentValidationState,
|
||||||
)
|
)
|
||||||
@@ -191,7 +183,6 @@ class DefaultContentScannerServiceTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
service.scan(
|
service.scan(
|
||||||
eventId = AN_EVENT_ID,
|
|
||||||
mediaSources = listOf(mediaSource),
|
mediaSources = listOf(mediaSource),
|
||||||
contentValidationState = contentValidationState,
|
contentValidationState = contentValidationState,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -858,7 +858,7 @@ class MessageComposerPresenter(
|
|||||||
if (currentComposerMode is MessageComposerMode.Reply) {
|
if (currentComposerMode is MessageComposerMode.Reply) {
|
||||||
val mediaSources = currentComposerMode.replyToDetails.content()?.mediaSources() ?: return@launch
|
val mediaSources = currentComposerMode.replyToDetails.content()?.mediaSources() ?: return@launch
|
||||||
val contentValidationState = contentValidationCache[currentComposerMode.replyToDetails.eventId()]
|
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.
|
// When coming from edit, just clear the composer as it'd be weird to reset a volatile draft in this scenario.
|
||||||
if (currentComposerMode.isEditing) {
|
if (currentComposerMode.isEditing) {
|
||||||
|
|||||||
+1
-1
@@ -259,7 +259,7 @@ class TimelinePresenter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is TimelineEvent.ValidateMedia -> {
|
is TimelineEvent.ValidateMedia -> {
|
||||||
timelineProtectionState.eventSink(TimelineProtectionEvent.ValidateContent(event.eventId, event.mediaSources, event.validationState))
|
timelineProtectionState.eventSink(TimelineProtectionEvent.ValidateContent(event.mediaSources, event.validationState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -15,7 +15,6 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentVal
|
|||||||
sealed interface TimelineProtectionEvent {
|
sealed interface TimelineProtectionEvent {
|
||||||
data class ShowContent(val eventId: EventId?) : TimelineProtectionEvent
|
data class ShowContent(val eventId: EventId?) : TimelineProtectionEvent
|
||||||
data class ValidateContent(
|
data class ValidateContent(
|
||||||
val eventId: EventId,
|
|
||||||
val mediaSources: List<MediaSource>,
|
val mediaSources: List<MediaSource>,
|
||||||
val validationState: ContentValidationState,
|
val validationState: ContentValidationState,
|
||||||
) : TimelineProtectionEvent
|
) : TimelineProtectionEvent
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ class TimelineProtectionPresenter(
|
|||||||
allowedEvents += setOfNotNull(event.eventId)
|
allowedEvents += setOfNotNull(event.eventId)
|
||||||
}
|
}
|
||||||
is TimelineProtectionEvent.ValidateContent -> {
|
is TimelineProtectionEvent.ValidateContent -> {
|
||||||
contentScannerService.scan(event.eventId, event.mediaSources, event.validationState)
|
contentScannerService.scan(event.mediaSources, event.validationState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -316,7 +316,7 @@ class MessageComposerPresenterSlashCommandTest {
|
|||||||
notificationConversationService = notificationConversationService,
|
notificationConversationService = notificationConversationService,
|
||||||
slashCommandService = slashCommandService,
|
slashCommandService = slashCommandService,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
contentScannerService = { _, _, _ -> },
|
contentScannerService = { _, _ -> },
|
||||||
contentValidationCache = InMemoryEventContentValidationCache(),
|
contentValidationCache = InMemoryEventContentValidationCache(),
|
||||||
).apply {
|
).apply {
|
||||||
isTesting = true
|
isTesting = true
|
||||||
|
|||||||
+1
-1
@@ -1650,7 +1650,7 @@ class MessageComposerPresenterTest : RobolectricTest() {
|
|||||||
notificationConversationService = notificationConversationService,
|
notificationConversationService = notificationConversationService,
|
||||||
slashCommandService = slashCommandService,
|
slashCommandService = slashCommandService,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
contentScannerService = { _, _, _ -> },
|
contentScannerService = { _, _ -> },
|
||||||
contentValidationCache = InMemoryEventContentValidationCache(),
|
contentValidationCache = InMemoryEventContentValidationCache(),
|
||||||
).apply {
|
).apply {
|
||||||
isTesting = true
|
isTesting = true
|
||||||
|
|||||||
+2
-4
@@ -10,7 +10,6 @@ package io.element.android.features.messages.impl.timeline.protection
|
|||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.contentscanner.api.ContentScannerService
|
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.MediaPreviewConfig
|
||||||
import io.element.android.libraries.matrix.api.media.MediaPreviewService
|
import io.element.android.libraries.matrix.api.media.MediaPreviewService
|
||||||
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
@@ -104,7 +103,7 @@ class TimelineProtectionPresenterTest {
|
|||||||
val mediaPreviewConfig = MediaPreviewConfig(mediaPreviewValue = MediaPreviewValue.Private, hideInviteAvatar = false)
|
val mediaPreviewConfig = MediaPreviewConfig(mediaPreviewValue = MediaPreviewValue.Private, hideInviteAvatar = false)
|
||||||
val mediaPreviewService = FakeMediaPreviewService(mediaPreviewConfigFlow = MutableStateFlow(mediaPreviewConfig))
|
val mediaPreviewService = FakeMediaPreviewService(mediaPreviewConfigFlow = MutableStateFlow(mediaPreviewConfig))
|
||||||
val room = FakeBaseRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite), roomCoroutineScope = backgroundScope)
|
val room = FakeBaseRoom(initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite), roomCoroutineScope = backgroundScope)
|
||||||
val contentScannerService = lambdaRecorder { _: EventId, _: List<MediaSource>, state: ContentValidationState ->
|
val contentScannerService = lambdaRecorder { _: List<MediaSource>, state: ContentValidationState ->
|
||||||
state.update(url, ContentValidationValue.Valid)
|
state.update(url, ContentValidationValue.Valid)
|
||||||
}
|
}
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
@@ -119,7 +118,6 @@ class TimelineProtectionPresenterTest {
|
|||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(
|
initialState.eventSink(
|
||||||
TimelineProtectionEvent.ValidateContent(
|
TimelineProtectionEvent.ValidateContent(
|
||||||
eventId = AN_EVENT_ID,
|
|
||||||
mediaSources = listOf(MediaSource(url)),
|
mediaSources = listOf(MediaSource(url)),
|
||||||
validationState = validationState
|
validationState = validationState
|
||||||
)
|
)
|
||||||
@@ -135,7 +133,7 @@ class TimelineProtectionPresenterTest {
|
|||||||
private fun createPresenter(
|
private fun createPresenter(
|
||||||
room: BaseRoom = FakeBaseRoom(),
|
room: BaseRoom = FakeBaseRoom(),
|
||||||
mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(),
|
mediaPreviewService: MediaPreviewService = FakeMediaPreviewService(),
|
||||||
contentScannerService: ContentScannerService = ContentScannerService { _: EventId, _: List<MediaSource>, _: ContentValidationState -> },
|
contentScannerService: ContentScannerService = ContentScannerService { _: List<MediaSource>, _: ContentValidationState -> },
|
||||||
) = TimelineProtectionPresenter(
|
) = TimelineProtectionPresenter(
|
||||||
mediaPreviewService = mediaPreviewService,
|
mediaPreviewService = mediaPreviewService,
|
||||||
room = room,
|
room = room,
|
||||||
|
|||||||
+1
-1
@@ -189,7 +189,7 @@ fun ContentValidationState.collectOverallState(): State<ContentValidationValue>
|
|||||||
@Composable
|
@Composable
|
||||||
fun ContentValidationState.collectMediaState(url: String?): State<ContentValidationValue> {
|
fun ContentValidationState.collectMediaState(url: String?): State<ContentValidationValue> {
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
return remember { mutableStateOf(ContentValidationValue.Unknown) }
|
return remember { mutableStateOf(ContentValidationValue.Valid) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return produceState(getCurrentMediaState(url), key1 = url) {
|
return produceState(getCurrentMediaState(url), key1 = url) {
|
||||||
|
|||||||
+4
-5
@@ -84,9 +84,8 @@ class MediaGalleryPresenter(
|
|||||||
if (groupedItems is AsyncData.Success) {
|
if (groupedItems is AsyncData.Success) {
|
||||||
val items = groupedItems.data.getItems(mode)
|
val items = groupedItems.data.getItems(mode)
|
||||||
items.forEach { item ->
|
items.forEach { item ->
|
||||||
if (item is MediaItem.Event) {
|
if (item is MediaItem.Event && item.eventId() != null) {
|
||||||
val eventId = item.eventId() ?: return@forEach
|
coroutineScope.validateMedia(item)
|
||||||
coroutineScope.validateMedia(eventId, item)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +236,7 @@ class MediaGalleryPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.validateMedia(eventId: EventId, mediaItem: MediaItem.Event) {
|
private fun CoroutineScope.validateMedia(mediaItem: MediaItem.Event) {
|
||||||
launch {
|
launch {
|
||||||
val thumbnailSource = mediaItem.thumbnailSource()
|
val thumbnailSource = mediaItem.thumbnailSource()
|
||||||
val mediaSource = mediaItem.mediaSource()
|
val mediaSource = mediaItem.mediaSource()
|
||||||
@@ -248,7 +247,7 @@ class MediaGalleryPresenter(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
contentScannerService.scan(eventId, listOfNotNull(thumbnailSource, mediaSource).distinct(), contentValidationState)
|
contentScannerService.scan(listOfNotNull(thumbnailSource, mediaSource).distinct(), contentValidationState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-6
@@ -242,18 +242,15 @@ class MediaViewerDataSource(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateMedia(
|
fun validateMedia(mediaSource: MediaSource, thumbnailMediaSource: MediaSource?) {
|
||||||
eventId: EventId,
|
|
||||||
mediaSource: MediaSource,
|
|
||||||
) {
|
|
||||||
val validationState = mediaValidationState[mediaSource.safeUrl] ?: return
|
val validationState = mediaValidationState[mediaSource.safeUrl] ?: return
|
||||||
val currentState = validationState.getCurrentMediaState(mediaSource.safeUrl)
|
val currentState = validationState.getCurrentOverallState()
|
||||||
|
|
||||||
if (currentState.isLoading() || currentState.isValid()) {
|
if (currentState.isLoading() || currentState.isValid()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
contentScannerService.scan(eventId, listOf(mediaSource), validationState)
|
contentScannerService.scan(listOfNotNull(mediaSource, thumbnailMediaSource), validationState)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelLoadingMedia(data: MediaViewerPageData.MediaViewerData) {
|
fun cancelLoadingMedia(data: MediaViewerPageData.MediaViewerData) {
|
||||||
|
|||||||
+1
-1
@@ -31,5 +31,5 @@ sealed interface MediaViewerEvent {
|
|||||||
data class OnNavigateTo(val index: Int) : MediaViewerEvent
|
data class OnNavigateTo(val index: Int) : MediaViewerEvent
|
||||||
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvent
|
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvent
|
||||||
data class CancelLoadingMedia(val data: MediaViewerPageData.MediaViewerData) : 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
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -194,7 +194,7 @@ class MediaViewerPresenter(
|
|||||||
is MediaViewerEvent.LoadMore -> coroutineScope.launch {
|
is MediaViewerEvent.LoadMore -> coroutineScope.launch {
|
||||||
dataSource.loadMore(event.direction)
|
dataSource.loadMore(event.direction)
|
||||||
}
|
}
|
||||||
is MediaViewerEvent.ValidateMedia -> dataSource.validateMedia(event.eventId, event.mediaSource)
|
is MediaViewerEvent.ValidateMedia -> dataSource.validateMedia(event.mediaSource, event.thumbnailMediaSource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-16
@@ -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.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
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.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.MediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
|
||||||
@@ -217,20 +218,24 @@ fun MediaViewerView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is MediaViewerPageData.MediaViewerData -> {
|
is MediaViewerPageData.MediaViewerData -> {
|
||||||
val eventId = dataForPage.eventId
|
// 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.:
|
||||||
val isMediaValid = dataForPage.validationState.getCurrentMediaState(dataForPage.mediaSource.safeUrl).isValid()
|
// 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
|
// Check if the media to display is valid or dangerous
|
||||||
LaunchedEffect(eventId) {
|
LaunchedEffect(Unit) {
|
||||||
if (eventId != null) {
|
state.eventSink(
|
||||||
state.eventSink(MediaViewerEvent.ValidateMedia(eventId, dataForPage.mediaSource))
|
MediaViewerEvent.ValidateMedia(mediaSource = dataForPage.mediaSource, thumbnailMediaSource = dataForPage.thumbnailSource)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
|
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
|
||||||
val loadMediaOnVisibilityChangedModifier = remember(isMediaValid) {
|
val loadMediaOnVisibilityChangedModifier = remember(mediaValidationState) {
|
||||||
if (isMediaValid == true) {
|
if (mediaValidationState.isValid() && thumbnailValidationState.isValid()) {
|
||||||
Modifier.onVisibilityChanged(minDurationMs = 200L) { isVisible ->
|
Modifier.onVisibilityChanged(minDurationMs = 200L) { isVisible ->
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
state.eventSink(MediaViewerEvent.LoadMedia(dataForPage))
|
state.eventSink(MediaViewerEvent.LoadMedia(dataForPage))
|
||||||
@@ -383,6 +388,8 @@ private fun MediaViewerPage(
|
|||||||
) {
|
) {
|
||||||
val downloadedMedia by data.downloadedMedia
|
val downloadedMedia by data.downloadedMedia
|
||||||
val showProgress = rememberShowProgress(downloadedMedia)
|
val showProgress = rememberShowProgress(downloadedMedia)
|
||||||
|
val mediaValidationState by data.validationState.collectMediaState(data.mediaSource.safeUrl)
|
||||||
|
val thumbnailValidationState by data.validationState.collectMediaState(data.thumbnailSource?.safeUrl)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -390,7 +397,7 @@ private fun MediaViewerPage(
|
|||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = data.validationState.getCurrentMediaState(data.mediaSource.safeUrl).isLoading(),
|
visible = mediaValidationState.isLoading(),
|
||||||
modifier = Modifier.padding(top = containerPadding.calculateTopPadding()).align(Alignment.TopCenter),
|
modifier = Modifier.padding(top = containerPadding.calculateTopPadding()).align(Alignment.TopCenter),
|
||||||
enter = fadeIn(spring(stiffness = 500F)),
|
enter = fadeIn(spring(stiffness = 500F)),
|
||||||
exit = fadeOut(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 (mediaValidationState.isInvalid() || thumbnailValidationState.isInvalid()) {
|
||||||
|
|
||||||
if (isMediaValid == false) {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
) {
|
||||||
IconTitleSubtitleMolecule(
|
IconTitleSubtitleMolecule(
|
||||||
iconStyle = BigIcon.Style.AlertSolid,
|
iconStyle = BigIcon.Style.AlertSolid,
|
||||||
title = "The file is not safe",
|
title = stringResource(CommonStrings.content_scanner_unsafe_title),
|
||||||
subTitle = "Preview and download have been disabled.",
|
subTitle = stringResource(CommonStrings.content_scanner_unsafe_message),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else if (mediaValidationState.isValid() && thumbnailValidationState.isValid()) {
|
||||||
LocalMediaView(
|
LocalMediaView(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
isDisplayed = isDisplayed,
|
isDisplayed = isDisplayed,
|
||||||
@@ -462,6 +467,7 @@ private fun MediaViewerPage(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showError) {
|
if (showError) {
|
||||||
ErrorView(
|
ErrorView(
|
||||||
errorMessage = stringResource(id = CommonStrings.error_unknown),
|
errorMessage = stringResource(id = CommonStrings.error_unknown),
|
||||||
|
|||||||
+2
-2
@@ -74,7 +74,7 @@ class DefaultMediaViewerEntryPointTest {
|
|||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
enterpriseService = FakeEnterpriseService(),
|
enterpriseService = FakeEnterpriseService(),
|
||||||
contentValidationCache = NoopEventContentValidationCache(),
|
contentValidationCache = NoopEventContentValidationCache(),
|
||||||
contentScannerService = ContentScannerService { _, _, _ -> },
|
contentScannerService = ContentScannerService { _, _ -> },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val callback = object : MediaViewerEntryPoint.Callback {
|
val callback = object : MediaViewerEntryPoint.Callback {
|
||||||
@@ -123,7 +123,7 @@ class DefaultMediaViewerEntryPointTest {
|
|||||||
sessionId = A_SESSION_ID,
|
sessionId = A_SESSION_ID,
|
||||||
enterpriseService = FakeEnterpriseService(),
|
enterpriseService = FakeEnterpriseService(),
|
||||||
contentValidationCache = NoopEventContentValidationCache(),
|
contentValidationCache = NoopEventContentValidationCache(),
|
||||||
contentScannerService = ContentScannerService { _, _, _ -> },
|
contentScannerService = ContentScannerService { _, _ -> },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val callback = object : MediaViewerEntryPoint.Callback {
|
val callback = object : MediaViewerEntryPoint.Callback {
|
||||||
|
|||||||
+1
-1
@@ -534,7 +534,7 @@ class MediaGalleryPresenterTest {
|
|||||||
mediaLoader = matrixMediaLoader,
|
mediaLoader = matrixMediaLoader,
|
||||||
localMediaActions = localMediaActions,
|
localMediaActions = localMediaActions,
|
||||||
snackbarDispatcher = snackbarDispatcher,
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
contentScannerService = { _, _, _ -> },
|
contentScannerService = { _, _ -> },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -291,7 +291,7 @@ class MediaViewerDataSourceTest {
|
|||||||
localMediaFactory = localMediaFactory,
|
localMediaFactory = localMediaFactory,
|
||||||
systemClock = FakeSystemClock(),
|
systemClock = FakeSystemClock(),
|
||||||
pagerKeysHandler = PagerKeysHandler(),
|
pagerKeysHandler = PagerKeysHandler(),
|
||||||
contentScannerService = { _, _, _ -> },
|
contentScannerService = { _, _ -> },
|
||||||
contentValidationCache = NoopEventContentValidationCache(),
|
contentValidationCache = NoopEventContentValidationCache(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -970,7 +970,7 @@ internal fun TestScope.createMediaViewerPresenter(
|
|||||||
localMediaFactory = localMediaFactory,
|
localMediaFactory = localMediaFactory,
|
||||||
systemClock = FakeSystemClock(),
|
systemClock = FakeSystemClock(),
|
||||||
pagerKeysHandler = PagerKeysHandler(),
|
pagerKeysHandler = PagerKeysHandler(),
|
||||||
contentScannerService = { _, _, _ -> },
|
contentScannerService = { _, _ -> },
|
||||||
contentValidationCache = NoopEventContentValidationCache(),
|
contentValidationCache = NoopEventContentValidationCache(),
|
||||||
),
|
),
|
||||||
room = room,
|
room = room,
|
||||||
|
|||||||
+40
@@ -47,6 +47,24 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
class MediaViewerViewTest : RobolectricTest() {
|
class MediaViewerViewTest : RobolectricTest() {
|
||||||
private val mockMediaUrl: Uri = mockk("localMediaUri")
|
private val mockMediaUrl: Uri = mockk("localMediaUri")
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `displaying a media will automatically validate it`() = runAndroidComposeUiTest {
|
||||||
|
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||||
|
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
|
@Test
|
||||||
fun `clicking on back invokes expected callback`() = runAndroidComposeUiTest {
|
fun `clicking on back invokes expected callback`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
val eventsRecorder = EventsRecorder<MediaViewerEvent>()
|
||||||
@@ -59,6 +77,9 @@ class MediaViewerViewTest : RobolectricTest() {
|
|||||||
onBackClick = callback,
|
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
|
// Wait for enough time for the onVisibilityChanged modifier to trigger
|
||||||
mainClock.advanceTimeBy(200)
|
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
|
// Wait for enough time for the onVisibilityChanged modifier to trigger
|
||||||
mainClock.advanceTimeBy(200)
|
mainClock.advanceTimeBy(200)
|
||||||
|
|
||||||
@@ -157,9 +181,11 @@ class MediaViewerViewTest : RobolectricTest() {
|
|||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
clickOn(textRes)
|
clickOn(textRes)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
|
MediaViewerEvent.ValidateMedia(mediaSource = data.mediaSource, thumbnailMediaSource = data.thumbnailSource),
|
||||||
MediaViewerEvent.LoadMedia(data),
|
MediaViewerEvent.LoadMedia(data),
|
||||||
expectedEvent,
|
expectedEvent,
|
||||||
)
|
)
|
||||||
@@ -175,6 +201,10 @@ class MediaViewerViewTest : RobolectricTest() {
|
|||||||
setMediaViewerView(
|
setMediaViewerView(
|
||||||
state = state,
|
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
|
// Ensure that the action are visible
|
||||||
val resources = activity!!.resources
|
val resources = activity!!.resources
|
||||||
val contentDescription = resources.getString(CommonStrings.action_share)
|
val contentDescription = resources.getString(CommonStrings.action_share)
|
||||||
@@ -205,6 +235,10 @@ class MediaViewerViewTest : RobolectricTest() {
|
|||||||
state = state,
|
state = state,
|
||||||
onBackClick = callback,
|
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)
|
val imageContentDescription = activity!!.getString(CommonStrings.common_image)
|
||||||
onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) }
|
onNodeWithContentDescription(imageContentDescription).performTouchInput { swipeDown(startY = centerY) }
|
||||||
mainClock.advanceTimeBy(1_000)
|
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
|
// Wait for enough time for the onVisibilityChanged modifier to trigger
|
||||||
mainClock.advanceTimeBy(200)
|
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
|
// Wait for enough time for the onVisibilityChanged modifier to trigger
|
||||||
mainClock.advanceTimeBy(200)
|
mainClock.advanceTimeBy(200)
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
Reference in New Issue
Block a user