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:
Jorge Martin Espinosa
2026-07-27 18:16:35 +02:00
committed by GitHub
parent d8b01cf3e7
commit 0442fbd307
24 changed files with 92 additions and 65 deletions
@@ -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) {
@@ -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))
}
}
}
@@ -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<MediaSource>,
val validationState: ContentValidationState,
) : TimelineProtectionEvent
@@ -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)
}
}
}
@@ -316,7 +316,7 @@ class MessageComposerPresenterSlashCommandTest {
notificationConversationService = notificationConversationService,
slashCommandService = slashCommandService,
featureFlagService = featureFlagService,
contentScannerService = { _, _, _ -> },
contentScannerService = { _, _ -> },
contentValidationCache = InMemoryEventContentValidationCache(),
).apply {
isTesting = true
@@ -1650,7 +1650,7 @@ class MessageComposerPresenterTest : RobolectricTest() {
notificationConversationService = notificationConversationService,
slashCommandService = slashCommandService,
featureFlagService = featureFlagService,
contentScannerService = { _, _, _ -> },
contentScannerService = { _, _ -> },
contentValidationCache = InMemoryEventContentValidationCache(),
).apply {
isTesting = true
@@ -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<MediaSource>, state: ContentValidationState ->
val contentScannerService = lambdaRecorder { _: List<MediaSource>, 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<MediaSource>, _: ContentValidationState -> },
contentScannerService: ContentScannerService = ContentScannerService { _: List<MediaSource>, _: ContentValidationState -> },
) = TimelineProtectionPresenter(
mediaPreviewService = mediaPreviewService,
room = room,