Add support for non-video gifs in v3 media send flow.

This commit is contained in:
Alex Hart
2026-07-28 15:50:05 -03:00
parent 153f0fe842
commit fc25581a6f
5 changed files with 39 additions and 6 deletions
@@ -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()
@@ -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.
*/
@@ -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.
@@ -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<VideoEditorFragment?>(null) }
@@ -238,7 +253,7 @@ fun MediaEditScreen(
)
}
EditorState.VideoGif, null -> Unit
EditorState.VideoGif, EditorState.Gif, null -> Unit
}
AddAMessageRow(
@@ -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 <T> GlideImage(
@@ -42,6 +45,7 @@ fun <T> 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 <T> GlideImage(
error = error,
transition = transition,
diskCacheStrategy = diskCacheStrategy,
contentScale = contentScale,
modifier = modifier
)
}
@@ -96,7 +101,8 @@ private fun <T> 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<Drawable?>(null)
@@ -148,7 +154,7 @@ private fun <T> 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
)
}