Add content scanner support to media gallery and viewer (Element Pro) (#7233)

* Add content scanner support to media gallery and viewer

* Improve UI and previews

* Remove default content validation state in media content views

* Disable click for invalid media items in the gallery events

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa
2026-07-17 15:26:13 +02:00
committed by GitHub
parent 19724aaa78
commit 2705efad15
88 changed files with 892 additions and 441 deletions
@@ -22,8 +22,8 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentVal
fun TimelineItemAudioView(
content: TimelineItemAudioContent,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
contentValidationValue: ContentValidationValue,
modifier: Modifier = Modifier,
contentValidationValue: ContentValidationValue = ContentValidationValue.Valid,
) {
TimelineItemAttachmentView(
icon = CompoundIcons.Audio(),
@@ -43,5 +43,6 @@ internal fun TimelineItemAudioViewPreview(@PreviewParameter(TimelineItemAudioCon
TimelineItemAudioView(
content,
onContentLayoutChange = {},
contentValidationValue = ContentValidationValue.Valid,
)
}
@@ -27,8 +27,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun TimelineItemFileView(
content: TimelineItemFileContent,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
contentValidationValue: ContentValidationValue,
modifier: Modifier = Modifier,
contentValidationValue: ContentValidationValue = ContentValidationValue.Valid,
) {
TimelineItemAttachmentView(
icon = CompoundIcons.Attachment(),
@@ -48,6 +48,7 @@ internal fun TimelineItemFileViewPreview(@PreviewParameter(TimelineItemFileConte
TimelineItemFileView(
content,
onContentLayoutChange = {},
contentValidationValue = ContentValidationValue.Valid,
)
}
}
@@ -288,9 +288,15 @@ private fun GalleryItemCell(
Box(
modifier = modifier
.blurHashBackground(item.blurhash, alpha = 0.9f)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
.then(
if (itemContentValidationState.isValid()) {
Modifier.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
} else {
Modifier
}
),
contentAlignment = Alignment.Center,
) {
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -43,7 +44,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState
import io.element.android.libraries.matrix.ui.media.contentvalidation.LocalEventContentValidationState
import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState
import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopEventContentValidationCache
import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
@@ -55,8 +58,8 @@ fun TimelineItemImageView(
onContentClick: (() -> Unit)?,
onLongClick: (() -> Unit)?,
onShowContentClick: () -> Unit,
contentValidationState: ContentValidationState,
modifier: Modifier = Modifier,
contentValidationState: ContentValidationState = remember { DefaultContentValidationState() },
) {
val a11yLabel = stringResource(CommonStrings.common_image)
val description = content.caption?.let { "$a11yLabel: $it" } ?: a11yLabel
@@ -129,52 +132,56 @@ internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageCon
onShowContentClick = {},
onContentClick = {},
onLongClick = {},
contentValidationState = NoopContentValidationState(),
)
}
@PreviewsDayNight
@Composable
internal fun TimelineItemImageViewHideMediaContentPreview() = ElementPreview {
TimelineItemImageView(
content = aTimelineItemImageContent(),
hideMediaContent = true,
onShowContentClick = {},
onContentClick = {},
onLongClick = {},
)
TimelineItemImageView(
content = aTimelineItemImageContent(),
hideMediaContent = true,
onShowContentClick = {},
onContentClick = {},
onLongClick = {},
contentValidationState = NoopContentValidationState(),
)
}
@PreviewsDayNight
@Composable
internal fun ATimelineItemEventRowPreview() = ElementPreview {
Column {
sequenceOf(false, true).forEach { isMine ->
CompositionLocalProvider(LocalEventContentValidationState provides NoopEventContentValidationCache()) {
Column {
sequenceOf(false, true).forEach { isMine ->
ATimelineItemEventRow(
event = aTimelineItemEvent(
isMine = isMine,
content = aTimelineItemImageContent(
filename = "image.jpg",
caption = "A long caption that may wrap into several lines",
width = 40,
height = 20,
aspectRatio = 40f / 20f,
),
groupPosition = TimelineItemGroupPosition.Last,
),
)
}
ATimelineItemEventRow(
event = aTimelineItemEvent(
isMine = isMine,
isMine = false,
content = aTimelineItemImageContent(
filename = "image.jpg",
caption = "A long caption that may wrap into several lines",
width = 40,
height = 20,
aspectRatio = 40f / 20f,
caption = "Narrow image with null aspectRatio",
width = 80,
height = 150,
aspectRatio = null,
),
groupPosition = TimelineItemGroupPosition.Last,
),
)
}
ATimelineItemEventRow(
event = aTimelineItemEvent(
isMine = false,
content = aTimelineItemImageContent(
filename = "image.jpg",
caption = "Narrow image with null aspectRatio",
width = 80,
height = 150,
aspectRatio = null,
),
groupPosition = TimelineItemGroupPosition.Last,
),
)
}
}
@@ -39,7 +39,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState
import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState
import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState
import io.element.android.libraries.ui.strings.CommonStrings
@@ -52,8 +52,8 @@ fun TimelineItemStickerView(
onContentClick: (() -> Unit)?,
onLongClick: (() -> Unit)?,
onShowClick: () -> Unit,
contentValidationState: ContentValidationState,
modifier: Modifier = Modifier,
contentValidationState: ContentValidationState = remember { DefaultContentValidationState() },
) {
val description = content.bestDescription.takeIf { it.isNotEmpty() } ?: stringResource(CommonStrings.common_image)
@@ -130,5 +130,6 @@ internal fun TimelineItemStickerViewPreview(@PreviewParameter(TimelineItemSticke
onContentClick = {},
onLongClick = {},
onShowClick = {},
contentValidationState = NoopContentValidationState(),
)
}
@@ -51,7 +51,7 @@ import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_HEIGHT
import io.element.android.libraries.matrix.ui.media.MAX_THUMBNAIL_WIDTH
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationState
import io.element.android.libraries.matrix.ui.media.contentvalidation.DefaultContentValidationState
import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopContentValidationState
import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.libraries.ui.utils.a11y.isTalkbackActive
@@ -63,8 +63,8 @@ fun TimelineItemVideoView(
onContentClick: (() -> Unit)?,
onLongClick: (() -> Unit)?,
onShowContentClick: () -> Unit,
contentValidationState: ContentValidationState,
modifier: Modifier = Modifier,
contentValidationState: ContentValidationState = remember { DefaultContentValidationState() },
) {
val isTalkbackActive = isTalkbackActive()
val a11yLabel = stringResource(CommonStrings.common_video)
@@ -157,6 +157,7 @@ internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoCon
onShowContentClick = {},
onContentClick = {},
onLongClick = {},
contentValidationState = NoopContentValidationState(),
)
}
@@ -169,5 +170,6 @@ internal fun TimelineItemVideoViewHideMediaContentPreview() = ElementPreview {
onShowContentClick = {},
onContentClick = {},
onLongClick = {},
contentValidationState = NoopContentValidationState(),
)
}
@@ -64,8 +64,8 @@ fun TimelineItemVoiceView(
state: VoiceMessageState,
content: TimelineItemVoiceContent,
onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit,
contentValidationValue: ContentValidationValue,
modifier: Modifier = Modifier,
contentValidationValue: ContentValidationValue = ContentValidationValue.Valid,
) {
fun playPause() {
state.eventSink(VoiceMessageEvent.PlayPause)
@@ -289,6 +289,7 @@ internal fun TimelineItemVoiceViewPreview(
state = timelineItemVoiceViewParameters.state,
content = timelineItemVoiceViewParameters.content,
onContentLayoutChange = {},
contentValidationValue = ContentValidationValue.Valid,
)
}
@@ -302,6 +303,7 @@ internal fun TimelineItemVoiceViewUnifiedPreview() = ElementPreview {
state = it.state,
content = it.content,
onContentLayoutChange = {},
contentValidationValue = ContentValidationValue.Valid,
)
}
}