From fc25581a6fa696846223d1dd077577a1bb93ec96 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 28 Jul 2026 15:50:05 -0300 Subject: [PATCH] Add support for non-video gifs in v3 media send flow. --- .../mediasend/v3/MediaSendV3Repository.kt | 2 +- .../java/org/signal/mediasend/EditorState.kt | 7 +++++++ .../org/signal/mediasend/MediaSendViewModel.kt | 9 +++++++-- .../signal/mediasend/edit/MediaEditScreen.kt | 17 ++++++++++++++++- .../java/org/signal/glide/compose/GlideImage.kt | 10 ++++++++-- 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt index 28e2845c06..71f32fa631 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v3/MediaSendV3Repository.kt @@ -244,7 +244,7 @@ object MediaSendV3Repository : MediaSendRepository { val legacyState: Any? = when (state) { is EditorState.Image -> ImageEditorFragment.Data().apply { writeModel(state.model) } is EditorState.VideoTrim -> state.videoTrimData - EditorState.VideoGif -> null + EditorState.VideoGif, EditorState.Gif -> null } legacyState?.let { uri to it } }.toMap() diff --git a/feature/media-send/src/main/java/org/signal/mediasend/EditorState.kt b/feature/media-send/src/main/java/org/signal/mediasend/EditorState.kt index d97b4cdf15..68631fb9d3 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/EditorState.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/EditorState.kt @@ -100,6 +100,13 @@ sealed interface EditorState : Parcelable { @Parcelize data object VideoGif : EditorState + /** + * Animated gif state. Gifs are played back as-is rather than being routed through the image editor, so there is + * nothing to track beyond the type itself. + */ + @Parcelize + data object Gif : EditorState + /** * Image editor state. */ diff --git a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt index 569eb3cf71..dd6acc3447 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/MediaSendViewModel.kt @@ -456,9 +456,14 @@ class MediaSendViewModel( .filter { isGifVideo(it) } .associate { it.uri to EditorState.VideoGif } + val initializedGifEditorStates = filterResult.filteredMedia + .filterNot { snapshot.editorStateMap.containsKey(it.uri) } + .filter { ContentTypeUtil.isGif(it.contentType) } + .associate { it.uri to EditorState.Gif } + val initializedImageEditorStates = filterResult.filteredMedia .filterNot { snapshot.editorStateMap.containsKey(it.uri) } - .filter { ContentTypeUtil.isImageType(it.contentType) } + .filter { ContentTypeUtil.isImageAndNotGif(it.contentType) } .associate { image -> // TODO - this should likely be in a repository? val editorModel = EditorModel.create(0x0) @@ -495,7 +500,7 @@ class MediaSendViewModel( copy( selectedMedia = filterResult.filteredMedia, focusedMedia = newFocus, - editorStateMap = editorStateMap + initializedVideoEditorStates + initializedVideoGifEditorStates + initializedImageEditorStates, + editorStateMap = editorStateMap + initializedVideoEditorStates + initializedVideoGifEditorStates + initializedGifEditorStates + initializedImageEditorStates, // Re-bind to the populated instance by URI: population fills in a video's 0x0 dimensions, producing a new // Media that no longer equals the pre-population capture, which would otherwise leak past equality-based // removal on back. Cleared once more than the capture is selected. diff --git a/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt index a3b1cadaf7..ff7eecc803 100644 --- a/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt +++ b/feature/media-send/src/main/java/org/signal/mediasend/edit/MediaEditScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.unit.dp import androidx.fragment.compose.AndroidFragment @@ -40,6 +41,9 @@ import org.signal.core.ui.WindowBreakpoint import org.signal.core.ui.compose.AllDevicePreviews import org.signal.core.ui.compose.Previews import org.signal.core.ui.rememberWindowBreakpoint +import org.signal.glide.compose.GlideImage +import org.signal.glide.compose.GlideImageScaleType +import org.signal.glide.decryptableuri.DecryptableUri import org.signal.imageeditor.core.model.EditorModel import org.signal.mediasend.EditorState import org.signal.mediasend.MediaSendDependencies @@ -117,6 +121,17 @@ fun MediaEditScreen( ) } + EditorState.Gif -> { + if (!LocalInspectionMode.current) { + GlideImage( + model = DecryptableUri(uri), + scaleType = GlideImageScaleType.FIT_CENTER, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxSize() + ) + } + } + is EditorState.VideoTrim, EditorState.VideoGif -> { val media = state.selectedMedia[index] var videoEditorFragment by remember(media.uri) { mutableStateOf(null) } @@ -238,7 +253,7 @@ fun MediaEditScreen( ) } - EditorState.VideoGif, null -> Unit + EditorState.VideoGif, EditorState.Gif, null -> Unit } AddAMessageRow( diff --git a/lib/glide/src/main/java/org/signal/glide/compose/GlideImage.kt b/lib/glide/src/main/java/org/signal/glide/compose/GlideImage.kt index 36a406525c..0c80499f41 100644 --- a/lib/glide/src/main/java/org/signal/glide/compose/GlideImage.kt +++ b/lib/glide/src/main/java/org/signal/glide/compose/GlideImage.kt @@ -31,6 +31,9 @@ import org.signal.glide.apng.ApngOptions /** * Our very own GlideImage. The GlideImage composable provided by the bumptech library is not suitable because it was is using our encrypted cache decoder/encoder. + * + * @param contentScale How the loaded drawable is scaled into the available space. Ignored when [enableApngAnimation] is + * set, as that path hands scaling to the underlying [ImageView] via [scaleType]. */ @Composable fun GlideImage( @@ -42,6 +45,7 @@ fun GlideImage( error: Drawable? = fallback, transition: TransitionOptions<*, Drawable>? = null, diskCacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL, + contentScale: ContentScale = ContentScale.Crop, enableApngAnimation: Boolean = false ) { if (enableApngAnimation) { @@ -82,6 +86,7 @@ fun GlideImage( error = error, transition = transition, diskCacheStrategy = diskCacheStrategy, + contentScale = contentScale, modifier = modifier ) } @@ -96,7 +101,8 @@ private fun GlideImage( fallback: Drawable? = null, error: Drawable? = fallback, transition: TransitionOptions<*, Drawable>? = null, - diskCacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL + diskCacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL, + contentScale: ContentScale = ContentScale.Crop ) { var drawable by remember { mutableStateOf(null) @@ -148,7 +154,7 @@ private fun GlideImage( Image( painter = rememberDrawablePainter(drawable), contentDescription = null, - contentScale = if (model == null) ContentScale.Inside else ContentScale.Crop, + contentScale = if (model == null) ContentScale.Inside else contentScale, modifier = modifier ) }