From c1c08917ced5ccc102a28be10f0b31970d31c9a9 Mon Sep 17 00:00:00 2001 From: jbsession Date: Thu, 19 Mar 2026 13:47:21 +0800 Subject: [PATCH 1/4] Coil Video decoder --- app/build.gradle.kts | 1 + .../thoughtcrime/securesms/coil/CoilModule.kt | 2 ++ .../securesms/mediasend/MediaSendViewModel.kt | 10 ------- .../securesms/mediasend/compose/Components.kt | 26 ++++++++++++++++--- gradle/libs.versions.toml | 1 + 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e5be661f0b..eea59f4252 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -406,6 +406,7 @@ dependencies { implementation(libs.glide.compose) implementation(libs.coil.compose) implementation(libs.coil.gif) + implementation(libs.coil.video) implementation(libs.android.image.cropper) implementation(libs.subsampling.scale.image.view) { exclude(group = "com.android.support", module = "support-annotations") diff --git a/app/src/main/java/org/thoughtcrime/securesms/coil/CoilModule.kt b/app/src/main/java/org/thoughtcrime/securesms/coil/CoilModule.kt index 99089c45cb..dc64ac9cc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/coil/CoilModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/coil/CoilModule.kt @@ -9,6 +9,7 @@ import coil3.gif.AnimatedImageDecoder import coil3.gif.GifDecoder import coil3.memory.MemoryCache import coil3.request.crossfade +import coil3.video.VideoFrameDecoder import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -50,6 +51,7 @@ class CoilModule { } add(BitmapFactoryDecoder.Factory()) + add(VideoFrameDecoder.Factory()) } .build() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.kt index 32336fbd5b..3115302e1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.kt @@ -69,9 +69,6 @@ class MediaSendViewModel @Inject constructor( private val positionLiveData: LiveData = uiState.map { it.position }.asLiveData() - private val foldersLiveData: LiveData> = - uiState.map { it.folders }.asLiveData() - private val countButtonStateLiveData: LiveData = uiState.map { CountButtonState(it.count, it.countVisibility) } .asLiveData() @@ -378,13 +375,6 @@ class MediaSendViewModel @Inject constructor( return uiState.map { it.bucketMedia }.asLiveData() } - fun getFolders(): LiveData> { - repository.getFolders(context) { value -> - _uiState.update { it.copy(folders = value) } - } - return foldersLiveData - } - fun getCountButtonState(): LiveData { return countButtonStateLiveData } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt index 187cfcaeaa..77b7570e32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import coil3.compose.AsyncImage import coil3.request.ImageRequest +import coil3.video.VideoFrameDecoder import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import network.loki.messenger.R import org.thoughtcrime.securesms.mediasend.Media @@ -58,6 +59,20 @@ fun MediaFolderCell( qaTag : String, onClick: () -> Unit ) { + val context = LocalContext.current + val thumbnailMimeType = thumbnailUri?.let { MediaUtil.getMimeType(context, it) } + + // our thumbnails do not have file extensions so we need to check for the mimetype + // then explicitly set the decoder for the request + val folderThumbnailRequest = ImageRequest.Builder(context) + .data(thumbnailUri) + .apply { + if (MediaUtil.isVideoType(thumbnailMimeType)) { + decoderFactory(VideoFrameDecoder.Factory()) + } + } + .build() + Box( modifier = Modifier .fillMaxWidth() @@ -68,9 +83,7 @@ fun MediaFolderCell( AsyncImage( modifier = Modifier.fillMaxWidth(), contentScale = ContentScale.Crop, - model = ImageRequest.Builder(LocalContext.current) - .data(thumbnailUri) - .build(), + model = folderThumbnailRequest, contentDescription = null, ) @@ -169,6 +182,11 @@ fun MediaPickerItemCell( contentScale = ContentScale.Crop, model = ImageRequest.Builder(LocalContext.current) .data(media.uri) + .apply { + if (MediaUtil.isVideoType(media.mimeType)) { + decoderFactory(VideoFrameDecoder.Factory()) + } + } .build(), contentDescription = null, ) @@ -186,7 +204,7 @@ fun MediaPickerItemCell( Image( painter = painterResource(R.drawable.triangle_right), contentDescription = null, - modifier = Modifier.size(LocalDimensions.current.iconMedium), + modifier = Modifier.size(LocalDimensions.current.iconSmall), colorFilter = ColorFilter.tint(LocalColors.current.accent) // match @color/core_blue-ish ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b68f4a8a16..1ad3d5d6ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -173,6 +173,7 @@ google-play-review-ktx = { module = "com.google.android.play:review-ktx", versio sqlite-web-viewer = { module = "io.github.simophin:sqlite-web-viewer", version = "0.2.0" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilVersion" } coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coilVersion" } +coil-video = {module = "io.coil-kt.coil3:coil-video", version.ref = "coilVersion" } android-billing = { module = "com.android.billingclient:billing", version.ref = "billingVersion" } android-billing-ktx = { module = "com.android.billingclient:billing-ktx", version.ref = "billingVersion" } mockk = { module = "io.mockk:mockk", version = "1.14.9" } From 980f01d4aa2be04f18f3af62c21a710b690f90c7 Mon Sep 17 00:00:00 2001 From: jbsession Date: Thu, 19 Mar 2026 14:03:23 +0800 Subject: [PATCH 2/4] Fix vide thumbnails --- .../securesms/mediasend/compose/Components.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt index 77b7570e32..3a12eaa33a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -62,7 +63,7 @@ fun MediaFolderCell( val context = LocalContext.current val thumbnailMimeType = thumbnailUri?.let { MediaUtil.getMimeType(context, it) } - // our thumbnails do not have file extensions so we need to check for the mimetype + // our URI does not have a file extension so we need to check for the mimetype // then explicitly set the decoder for the request val folderThumbnailRequest = ImageRequest.Builder(context) .data(thumbnailUri) @@ -198,13 +199,14 @@ fun MediaPickerItemCell( .align(Alignment.Center) .size(36.dp) .clip(CircleShape) - .background(Color.White), + .background(Color.White) + .padding(start = LocalDimensions.current.xxxsSpacing), contentAlignment = Alignment.Center ) { Image( painter = painterResource(R.drawable.triangle_right), contentDescription = null, - modifier = Modifier.size(LocalDimensions.current.iconSmall), + modifier = Modifier.height(LocalDimensions.current.iconXSmall), colorFilter = ColorFilter.tint(LocalColors.current.accent) // match @color/core_blue-ish ) } From d7764ae82afaf57cad53bda4d911c9807d32ddc2 Mon Sep 17 00:00:00 2001 From: jbsession Date: Thu, 19 Mar 2026 14:22:07 +0800 Subject: [PATCH 3/4] Fix wrong modifier --- .../org/thoughtcrime/securesms/mediasend/compose/Components.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt index 3a12eaa33a..94403beec6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt @@ -206,7 +206,7 @@ fun MediaPickerItemCell( Image( painter = painterResource(R.drawable.triangle_right), contentDescription = null, - modifier = Modifier.height(LocalDimensions.current.iconXSmall), + modifier = Modifier.size(LocalDimensions.current.iconXSmall), colorFilter = ColorFilter.tint(LocalColors.current.accent) // match @color/core_blue-ish ) } From 94d801be693b7b87594c12e82a119d26b44bec7d Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 20 Mar 2026 12:47:39 +0800 Subject: [PATCH 4/4] Optimizations --- .../securesms/mediasend/compose/Components.kt | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt index 94403beec6..f3627e211b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/compose/Components.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -63,16 +64,20 @@ fun MediaFolderCell( val context = LocalContext.current val thumbnailMimeType = thumbnailUri?.let { MediaUtil.getMimeType(context, it) } + val videoDecoderFactory = remember { VideoFrameDecoder.Factory() } + // our URI does not have a file extension so we need to check for the mimetype // then explicitly set the decoder for the request - val folderThumbnailRequest = ImageRequest.Builder(context) - .data(thumbnailUri) - .apply { - if (MediaUtil.isVideoType(thumbnailMimeType)) { - decoderFactory(VideoFrameDecoder.Factory()) + val folderThumbnailRequest = remember(context, thumbnailUri) { + ImageRequest.Builder(context) + .data(thumbnailUri) + .apply { + if (MediaUtil.isVideoType(thumbnailMimeType)) { + decoderFactory(videoDecoderFactory) + } } - } - .build() + .build() + } Box( modifier = Modifier @@ -156,6 +161,21 @@ fun MediaPickerItemCell( showSelectionOn: Boolean = false, canLongPress: Boolean = true ) { + val context = LocalContext.current + + val videoDecoderFactory = remember { VideoFrameDecoder.Factory() } + + val mediaRequest = remember(context, media.uri, media.mimeType) { + ImageRequest.Builder(context) + .data(media.uri) + .apply { + if (MediaUtil.isVideoType(media.mimeType)) { + decoderFactory(videoDecoderFactory) + } + } + .build() + } + Box( modifier = modifier .aspectRatio(1f) @@ -181,14 +201,7 @@ fun MediaPickerItemCell( AsyncImage( modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, - model = ImageRequest.Builder(LocalContext.current) - .data(media.uri) - .apply { - if (MediaUtil.isVideoType(media.mimeType)) { - decoderFactory(VideoFrameDecoder.Factory()) - } - } - .build(), + model = mediaRequest, contentDescription = null, )