mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Content scanner (Element Pro): scan timeline events for unsafe content (#7159)
* Simplify how event content with captions are displayed The caption component will now be added in `TimelineItemEventContentView` instead of separately in each `TimelineItem__View` component * Make sure `CoilMediaFetcher` methods to fetch media throw errors We need to catch them later to check if the content scanner marked some thumbnails or medias as invalid/unsafe in `AsyncImage` instances * Add a `ContentScannerService` that allows us to manually scan a list of medias in an event and then update and store the results The results take the form of `ContentValidationValue`, stored in a `ContentValidationState` instance tied to the event * Create `EventContentValidationCache` to keep the `ContentValidationStates` for events in a room * Expose `EventContentValidationCache` to Compose using `LocalEventContentValidationState` Also add a couple of helper functions to avoid having to manually remember the states * Use the new components to display invalid content in the timeline items * Add `blurhash` to most media items so we can use it while validating the thumbnails/medias * Fix custom layout issues with screenshot and UI tests * Also check the validation state for replied-to events in the composer * Fix and add tests * Handle unrecoverable content validation errors too: display a 'not found' UI * Improve the performance of `EqualWidthColumn` for the single item case * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
56cf00560e
commit
0d03472c60
+2
@@ -27,6 +27,8 @@ class DefaultMediaViewerEntryPoint : MediaViewerEntryPoint {
|
||||
),
|
||||
mediaSource = MediaSource(url = avatarUrl),
|
||||
thumbnailSource = null,
|
||||
// Maybe add avatar blurhash if available?
|
||||
blurHash = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -116,6 +116,7 @@ class EventItemFactory(
|
||||
),
|
||||
mediaSource = c.source,
|
||||
thumbnailSource = c.info?.thumbnailSource,
|
||||
blurHash = c.info?.blurhash,
|
||||
)
|
||||
}
|
||||
is GalleryItemType.Video -> {
|
||||
@@ -136,6 +137,7 @@ class EventItemFactory(
|
||||
),
|
||||
mediaSource = c.source,
|
||||
thumbnailSource = c.info?.thumbnailSource,
|
||||
blurHash = c.info?.blurhash,
|
||||
)
|
||||
}
|
||||
is GalleryItemType.Audio -> {
|
||||
@@ -242,6 +244,7 @@ class EventItemFactory(
|
||||
),
|
||||
mediaSource = type.source,
|
||||
thumbnailSource = type.info?.thumbnailSource,
|
||||
blurHash = type.info?.blurhash,
|
||||
))
|
||||
is StickerMessageType -> listOf(MediaItem.Image(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
@@ -264,6 +267,7 @@ class EventItemFactory(
|
||||
),
|
||||
mediaSource = type.source,
|
||||
thumbnailSource = type.info?.thumbnailSource,
|
||||
blurHash = type.info?.blurhash,
|
||||
))
|
||||
is VideoMessageType -> listOf(MediaItem.Video(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
@@ -286,6 +290,7 @@ class EventItemFactory(
|
||||
),
|
||||
mediaSource = type.source,
|
||||
thumbnailSource = type.info?.thumbnailSource,
|
||||
blurHash = type.info?.blurhash,
|
||||
))
|
||||
is VoiceMessageType -> listOf(MediaItem.Voice(
|
||||
id = currentTimelineItem.uniqueId,
|
||||
|
||||
+4
@@ -34,6 +34,7 @@ import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
|
||||
import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryNode
|
||||
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
|
||||
import io.element.android.libraries.mediaviewer.impl.model.blurHash
|
||||
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.mediaSource
|
||||
@@ -68,6 +69,7 @@ class MediaGalleryFlowNode(
|
||||
val mediaInfo: MediaInfo,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val blurHash: String?,
|
||||
) : NavTarget
|
||||
}
|
||||
|
||||
@@ -104,6 +106,7 @@ class MediaGalleryFlowNode(
|
||||
mediaInfo = item.mediaInfo(),
|
||||
mediaSource = item.mediaSource(),
|
||||
thumbnailSource = item.thumbnailSource(),
|
||||
blurHash = item.blurHash(),
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -134,6 +137,7 @@ class MediaGalleryFlowNode(
|
||||
mediaInfo = navTarget.mediaInfo,
|
||||
mediaSource = navTarget.mediaSource,
|
||||
thumbnailSource = navTarget.thumbnailSource,
|
||||
blurHash = navTarget.blurHash,
|
||||
),
|
||||
callback = callback,
|
||||
)
|
||||
|
||||
+2
@@ -34,6 +34,7 @@ import coil3.compose.AsyncImage
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.components.blurhash.blurHashBackground
|
||||
import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -51,6 +52,7 @@ fun VideoItemView(
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.blurHashBackground(blurHash = video.blurHash)
|
||||
.aspectRatio(1f)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
|
||||
+10
@@ -35,6 +35,7 @@ sealed interface MediaItem {
|
||||
val mediaInfo: MediaInfo,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val blurHash: String?,
|
||||
) : Event {
|
||||
val thumbnailMediaRequestData: MediaRequestData
|
||||
get() = MediaRequestData(thumbnailSource ?: mediaSource, MediaRequestData.Kind.Thumbnail(100))
|
||||
@@ -46,6 +47,7 @@ sealed interface MediaItem {
|
||||
val mediaInfo: MediaInfo,
|
||||
val mediaSource: MediaSource,
|
||||
val thumbnailSource: MediaSource?,
|
||||
val blurHash: String?,
|
||||
) : Event {
|
||||
val thumbnailMediaRequestData: MediaRequestData
|
||||
get() = MediaRequestData(thumbnailSource ?: mediaSource, MediaRequestData.Kind.Thumbnail(100))
|
||||
@@ -124,3 +126,11 @@ fun MediaItem.Event.thumbnailSource(): MediaSource? {
|
||||
is MediaItem.Voice -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun MediaItem.Event.blurHash(): String? {
|
||||
return when (this) {
|
||||
is MediaItem.Image -> blurHash
|
||||
is MediaItem.Video -> blurHash
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -34,6 +34,7 @@ fun aMediaItemImage(
|
||||
),
|
||||
mediaSource = MediaSource(mediaSourceUrl),
|
||||
thumbnailSource = null,
|
||||
blurHash = null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,6 +51,7 @@ fun aMediaItemVideo(
|
||||
),
|
||||
mediaSource = mediaSource,
|
||||
thumbnailSource = null,
|
||||
blurHash = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -63,6 +63,7 @@ class GalleryMediaGalleryDataSource(
|
||||
mediaInfo = itemMediaInfo,
|
||||
mediaSource = galleryItem.mediaSource,
|
||||
thumbnailSource = galleryItem.thumbnailSource,
|
||||
blurHash = galleryItem.blurHash,
|
||||
)
|
||||
GalleryItemData.Type.Audio -> MediaItem.Audio(
|
||||
id = id,
|
||||
@@ -82,6 +83,7 @@ class GalleryMediaGalleryDataSource(
|
||||
mediaInfo = itemMediaInfo,
|
||||
mediaSource = galleryItem.mediaSource,
|
||||
thumbnailSource = galleryItem.thumbnailSource,
|
||||
blurHash = galleryItem.blurHash,
|
||||
)
|
||||
}
|
||||
mixedItems.add(mediaItem)
|
||||
|
||||
+3
@@ -63,6 +63,7 @@ class SingleMediaGalleryDataSource(
|
||||
),
|
||||
mediaSource = params.mediaSource,
|
||||
thumbnailSource = params.thumbnailSource,
|
||||
blurHash = params.blurHash,
|
||||
)
|
||||
),
|
||||
fileItems = persistentListOf(),
|
||||
@@ -79,6 +80,7 @@ fun MediaViewerEntryPoint.Params.RoomMedia.toMediaItem() = when {
|
||||
mediaInfo = mediaInfo,
|
||||
mediaSource = mediaSource,
|
||||
thumbnailSource = thumbnailSource,
|
||||
blurHash = blurHash,
|
||||
)
|
||||
}
|
||||
mediaInfo.mimeType.isMimeTypeVideo() -> {
|
||||
@@ -88,6 +90,7 @@ fun MediaViewerEntryPoint.Params.RoomMedia.toMediaItem() = when {
|
||||
mediaInfo = mediaInfo,
|
||||
mediaSource = mediaSource,
|
||||
thumbnailSource = thumbnailSource,
|
||||
blurHash = blurHash,
|
||||
)
|
||||
}
|
||||
mediaInfo.mimeType.isMimeTypeAudio() -> {
|
||||
|
||||
+1
@@ -141,6 +141,7 @@ class DefaultMediaViewerEntryPointTest {
|
||||
avatarInfo = AvatarInfo(filename = "avatar.png"),
|
||||
mediaSource = MediaSource(url = "avatarUrl"),
|
||||
thumbnailSource = null,
|
||||
blurHash = null,
|
||||
)
|
||||
)
|
||||
assertThat(result.plugins).contains(callback)
|
||||
|
||||
+5
@@ -226,6 +226,7 @@ class EventItemFactoryTest {
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
blurHash = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -326,6 +327,7 @@ class EventItemFactoryTest {
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
blurHash = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -437,6 +439,7 @@ class EventItemFactoryTest {
|
||||
),
|
||||
mediaSource = MediaSource("image_url"),
|
||||
thumbnailSource = MediaSource("thumbnail_url"),
|
||||
blurHash = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -498,6 +501,7 @@ class EventItemFactoryTest {
|
||||
),
|
||||
mediaSource = MediaSource("video_url"),
|
||||
thumbnailSource = MediaSource("thumbnail_url"),
|
||||
blurHash = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -744,6 +748,7 @@ class EventItemFactoryTest {
|
||||
),
|
||||
mediaSource = MediaSource(""),
|
||||
thumbnailSource = null,
|
||||
blurHash = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -249,6 +249,7 @@ class TimelineMediaGalleryDataSourceTest {
|
||||
),
|
||||
mediaSource = MediaSource("url"),
|
||||
thumbnailSource = MediaSource("url_thumbnail"),
|
||||
blurHash = A_BLUR_HASH,
|
||||
)
|
||||
),
|
||||
fileItems = persistentListOf()
|
||||
|
||||
+10
@@ -91,6 +91,7 @@ class GalleryMediaGalleryDataSourceTest {
|
||||
mediaSource = MediaSource("image_url"),
|
||||
thumbnailSource = MediaSource("thumbnail_url"),
|
||||
type = GalleryItemData.Type.Image,
|
||||
blurHash = null,
|
||||
)
|
||||
),
|
||||
galleryInfo = aGalleryInfo(),
|
||||
@@ -104,6 +105,7 @@ class GalleryMediaGalleryDataSourceTest {
|
||||
mediaInfo = expectedMediaInfo("image.jpg", MimeTypes.Jpeg),
|
||||
mediaSource = MediaSource("image_url"),
|
||||
thumbnailSource = MediaSource("thumbnail_url"),
|
||||
blurHash = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -119,6 +121,7 @@ class GalleryMediaGalleryDataSourceTest {
|
||||
mediaSource = MediaSource("video_url"),
|
||||
thumbnailSource = MediaSource("thumbnail_url"),
|
||||
type = GalleryItemData.Type.Video,
|
||||
blurHash = null,
|
||||
)
|
||||
),
|
||||
galleryInfo = aGalleryInfo(),
|
||||
@@ -132,6 +135,7 @@ class GalleryMediaGalleryDataSourceTest {
|
||||
mediaInfo = expectedMediaInfo("video.mp4", MimeTypes.Mp4),
|
||||
mediaSource = MediaSource("video_url"),
|
||||
thumbnailSource = MediaSource("thumbnail_url"),
|
||||
blurHash = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -147,6 +151,7 @@ class GalleryMediaGalleryDataSourceTest {
|
||||
mediaSource = MediaSource("audio_url"),
|
||||
thumbnailSource = null,
|
||||
type = GalleryItemData.Type.Audio,
|
||||
blurHash = null,
|
||||
)
|
||||
),
|
||||
galleryInfo = aGalleryInfo(),
|
||||
@@ -174,6 +179,7 @@ class GalleryMediaGalleryDataSourceTest {
|
||||
mediaSource = MediaSource("file_url"),
|
||||
thumbnailSource = null,
|
||||
type = GalleryItemData.Type.File,
|
||||
blurHash = null,
|
||||
)
|
||||
),
|
||||
galleryInfo = aGalleryInfo(),
|
||||
@@ -201,6 +207,7 @@ class GalleryMediaGalleryDataSourceTest {
|
||||
mediaSource = MediaSource("image_url"),
|
||||
thumbnailSource = null,
|
||||
type = GalleryItemData.Type.Image,
|
||||
blurHash = null,
|
||||
)
|
||||
),
|
||||
galleryInfo = aGalleryInfo(),
|
||||
@@ -222,6 +229,7 @@ class GalleryMediaGalleryDataSourceTest {
|
||||
mediaSource = MediaSource("image_url"),
|
||||
thumbnailSource = null,
|
||||
type = GalleryItemData.Type.Image,
|
||||
blurHash = null,
|
||||
),
|
||||
GalleryItemData(
|
||||
filename = "document.pdf",
|
||||
@@ -229,6 +237,7 @@ class GalleryMediaGalleryDataSourceTest {
|
||||
mediaSource = MediaSource("file_url"),
|
||||
thumbnailSource = null,
|
||||
type = GalleryItemData.Type.File,
|
||||
blurHash = null,
|
||||
),
|
||||
GalleryItemData(
|
||||
filename = "video.mp4",
|
||||
@@ -236,6 +245,7 @@ class GalleryMediaGalleryDataSourceTest {
|
||||
mediaSource = MediaSource("video_url"),
|
||||
thumbnailSource = null,
|
||||
type = GalleryItemData.Type.Video,
|
||||
blurHash = null,
|
||||
),
|
||||
),
|
||||
galleryInfo = aGalleryInfo(),
|
||||
|
||||
+2
@@ -114,6 +114,7 @@ class MediaViewerPresenterTest {
|
||||
avatarInfo = AvatarInfo(filename = "avatar.png"),
|
||||
mediaSource = aMediaSource(),
|
||||
thumbnailSource = null,
|
||||
blurHash = null,
|
||||
),
|
||||
room = FakeJoinedRoom(
|
||||
baseRoom = FakeBaseRoom(
|
||||
@@ -983,4 +984,5 @@ internal fun createMediaViewerEntryPointParams(
|
||||
mediaInfo = TESTED_MEDIA_INFO,
|
||||
mediaSource = aMediaSource(),
|
||||
thumbnailSource = null,
|
||||
blurHash = null,
|
||||
)
|
||||
|
||||
+3
@@ -89,6 +89,7 @@ class SingleMediaGalleryDataSourceTest {
|
||||
mediaInfo = params.mediaInfo,
|
||||
mediaSource = params.mediaSource,
|
||||
thumbnailSource = params.thumbnailSource,
|
||||
blurHash = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -105,6 +106,7 @@ class SingleMediaGalleryDataSourceTest {
|
||||
mediaInfo = params.mediaInfo,
|
||||
mediaSource = params.mediaSource,
|
||||
thumbnailSource = params.thumbnailSource,
|
||||
blurHash = null,
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -175,5 +177,6 @@ class SingleMediaGalleryDataSourceTest {
|
||||
mediaInfo = mediaInfo,
|
||||
mediaSource = aMediaSource(url = "aUrl"),
|
||||
thumbnailSource = aMediaSource(url = "aThumbnailUrl"),
|
||||
blurHash = null,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user