From 5e38ae72fe64f0887d8aeef3fcfe85592a5239d7 Mon Sep 17 00:00:00 2001 From: Jens Zalzala Date: Sat, 14 Mar 2026 11:21:57 -0500 Subject: [PATCH 1/3] GIF animations autoplay in chat messages Hide filename under GIF images. Signed-off-by: Jens Zalzala --- .../messages/PreviewMessageViewHolder.kt | 27 ++++++++++++++++++- .../talk/chat/data/model/ChatMessage.kt | 13 +++++++++ .../nextcloud/talk/contacts/ImageRequest.kt | 21 +++++++++++---- .../talk/extensions/ImageViewExtensions.kt | 4 +-- .../nextcloud/talk/ui/ComposeChatAdapter.kt | 16 ++++++++++- 5 files changed, 72 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt index fda68f9f824..27f12e4d130 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt @@ -17,6 +17,7 @@ import android.os.Handler import android.util.Base64 import android.util.Log import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView @@ -47,6 +48,9 @@ import com.nextcloud.talk.utils.FileViewerUtils import com.nextcloud.talk.utils.FileViewerUtils.ProgressUi import com.nextcloud.talk.utils.message.MessageUtils import com.stfalcon.chatkit.messages.MessageHolders.IncomingImageMessageViewHolder +import coil.load +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.MimetypeUtils import io.reactivex.Single import io.reactivex.SingleObserver import io.reactivex.disposables.Disposable @@ -100,6 +104,23 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : super.onBind(message) image.minimumHeight = DisplayUtils.convertDpToPixel(MIN_IMAGE_HEIGHT, context!!).toInt() + // Reset state for view recycling + image.adjustViewBounds = false + messageText.visibility = View.VISIBLE + + // Check if image is GIF and load animated image + val mimetype = message.selectedIndividualHashMap?.get(KEY_MIMETYPE) + if (MimetypeUtils.isGif(mimetype ?: "") && message.imageUrl != null) { + image.adjustViewBounds = true + image.load(message.imageUrl) { + size(coil.size.Size.ORIGINAL) + addHeader( + "Authorization", + ApiUtils.getCredentials(message.activeUser!!.username, message.activeUser!!.token)!! + ) + } + } + if (message.lastEditTimestamp != 0L && !message.isDeleted) { time.text = dateUtils.getLocalTimeStringFromTimestamp(message.lastEditTimestamp!!) messageEditIndicator.visibility = View.VISIBLE @@ -110,7 +131,6 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : viewThemeUtils!!.platform.colorCircularProgressBar(progressBar!!, ColorRole.PRIMARY) clickView = image - messageText.visibility = View.VISIBLE if (message.getCalculateMessageType() === ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) { val chatActivity = commonMessageInterface as ChatActivity fileViewerUtils = FileViewerUtils(chatActivity, message.activeUser!!) @@ -118,6 +138,11 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : messageText.text = fileName + // hide filename display for GIF images + if (MimetypeUtils.isGif(mimetype ?: "")) { + messageText.visibility = View.INVISIBLE + } + if (message.activeUser != null && message.activeUser!!.username != null && message.activeUser!!.baseUrl != null diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt index c856ca6977d..0429589736f 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt @@ -21,6 +21,7 @@ import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.CapabilitiesUtil +import com.nextcloud.talk.utils.Mimetype import com.stfalcon.chatkit.commons.models.IUser import com.stfalcon.chatkit.commons.models.MessageContentType import java.security.MessageDigest @@ -246,6 +247,18 @@ data class ChatMessage( selectedIndividualHashMap = individualHashMap if (!isVoiceMessage) { if (activeUser != null && activeUser!!.baseUrl != null) { + val mimetype = individualHashMap["mimetype"] + val path = individualHashMap["path"] + if (mimetype == Mimetype.IMAGE_GIF && + path != null && + activeUser!!.username != null + ) { + return ApiUtils.getUrlForFileDownload( + activeUser!!.baseUrl!!, + activeUser!!.username!!, + path + ) + } return ApiUtils.getUrlForFilePreviewWithFileId( activeUser!!.baseUrl!!, individualHashMap["id"]!!, diff --git a/app/src/main/java/com/nextcloud/talk/contacts/ImageRequest.kt b/app/src/main/java/com/nextcloud/talk/contacts/ImageRequest.kt index 6d8eda446b8..cd5903bb3be 100644 --- a/app/src/main/java/com/nextcloud/talk/contacts/ImageRequest.kt +++ b/app/src/main/java/com/nextcloud/talk/contacts/ImageRequest.kt @@ -2,6 +2,7 @@ * Nextcloud Talk - Android Client * * SPDX-FileCopyrightText: 2024 Sowjanya Kota + * SPDX-FileCopyrightText: 2026 Jens Zalzala * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -26,13 +27,23 @@ fun loadImage(imageUri: String?, context: Context, errorPlaceholderImage: Int): } @Composable -fun load(imageUri: String?, context: Context, errorPlaceholderImage: Int): ImageRequest { - val imageRequest = ImageRequest.Builder(context) +fun load( + imageUri: String?, + context: Context, + errorPlaceholderImage: Int, + animated: Boolean = false, + authHeader: String? = null +): ImageRequest { + val builder = ImageRequest.Builder(context) .data(imageUri) .size(Size.ORIGINAL) - .transformations(RoundedCornersTransformation()) .error(errorPlaceholderImage) .placeholder(errorPlaceholderImage) - .build() - return imageRequest + if (!animated) { + builder.transformations(RoundedCornersTransformation()) + } + if (authHeader != null) { + builder.addHeader("Authorization", authHeader) + } + return builder.build() } diff --git a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt index 66b85848fbc..b02bd125a48 100644 --- a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt +++ b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt @@ -233,7 +233,7 @@ fun ImageView.loadThumbnail(url: String, user: User): io.reactivex.disposables.D requestBuilder.placeholder(LayerDrawable(layers)) if (url.startsWith(user.baseUrl!!) && - (url.contains("index.php/core/preview") || url.contains("/avatar/")) + (url.contains("index.php/core/preview") || url.contains("/avatar/") || url.contains("remote.php/dav/")) ) { requestBuilder.addHeader( "Authorization", @@ -259,7 +259,7 @@ fun ImageView.loadImage(url: String, user: User, placeholder: Drawable? = null): .transformations(RoundedCornersTransformation(ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL, ROUNDING_PIXEL)) if (url.startsWith(user.baseUrl!!) && - (url.contains("index.php/core/preview") || url.contains("/avatar/")) + (url.contains("index.php/core/preview") || url.contains("/avatar/") || url.contains("remote.php/dav/")) ) { requestBuilder.addHeader( "Authorization", diff --git a/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt b/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt index 8f2fb48b4bd..4667069f617 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt @@ -105,7 +105,9 @@ import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.models.json.opengraph.Reference import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DateUtils +import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType import com.nextcloud.talk.utils.message.MessageUtils @@ -809,7 +811,19 @@ class ComposeChatAdapter( val imageUri = message.imageUrl val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE] val drawableResourceId = getDrawableResourceIdForMimeType(mimetype) - val loadedImage = load(imageUri, LocalContext.current, drawableResourceId) + val isGif = mimetype == Mimetype.IMAGE_GIF + val authHeader = if (isGif) { + ApiUtils.getCredentials(currentUser.username, currentUser.token) + } else { + null + } + val loadedImage = load( + imageUri, + LocalContext.current, + drawableResourceId, + animated = isGif, + authHeader = authHeader + ) AsyncImage( model = loadedImage, From 092c87ecc06f2979e14c0b26a581e2f3c79b8f55 Mon Sep 17 00:00:00 2001 From: Jens Zalzala Date: Sat, 21 Mar 2026 14:15:09 -0500 Subject: [PATCH 2/3] Get max GIF size from config. Autoplay GIF file if within max GIF size. Signed-off-by: Jens Zalzala --- .../messages/PreviewMessageViewHolder.kt | 4 +-- .../talk/chat/data/model/ChatMessage.kt | 31 +++++++++++++------ .../nextcloud/talk/ui/ComposeChatAdapter.kt | 2 +- .../nextcloud/talk/utils/CapabilitiesUtil.kt | 16 ++++++++++ 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt index 27f12e4d130..318ee7cd6d2 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt @@ -110,7 +110,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : // Check if image is GIF and load animated image val mimetype = message.selectedIndividualHashMap?.get(KEY_MIMETYPE) - if (MimetypeUtils.isGif(mimetype ?: "") && message.imageUrl != null) { + if (message.imageUrl != null && message.shouldAutoplayGif()) { image.adjustViewBounds = true image.load(message.imageUrl) { size(coil.size.Size.ORIGINAL) @@ -139,7 +139,7 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : messageText.text = fileName // hide filename display for GIF images - if (MimetypeUtils.isGif(mimetype ?: "")) { + if (message.shouldAutoplayGif()) { messageText.visibility = View.INVISIBLE } diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt index 0429589736f..937c89ba760 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt @@ -237,6 +237,19 @@ data class ChatMessage( return false } + /** + * @return true if message is a GIF file, and size is below max-gif-size in the config + */ + fun shouldAutoplayGif(): Boolean { + val mimetype = selectedIndividualHashMap?.get("mimetype") + if (mimetype != Mimetype.IMAGE_GIF) return false + val user = activeUser ?: return false + val capabilities = user.capabilities?.spreedCapability ?: return false + val maxGifSize = CapabilitiesUtil.getMaxGifSize(capabilities) + val fileSize = selectedIndividualHashMap?.get("size")?.toLongOrNull() ?: return true + return fileSize in 1..maxGifSize + } + @Suppress("Detekt.NestedBlockDepth") override fun getImageUrl(): String? { if (messageParameters != null && messageParameters!!.size > 0) { @@ -247,17 +260,15 @@ data class ChatMessage( selectedIndividualHashMap = individualHashMap if (!isVoiceMessage) { if (activeUser != null && activeUser!!.baseUrl != null) { - val mimetype = individualHashMap["mimetype"] val path = individualHashMap["path"] - if (mimetype == Mimetype.IMAGE_GIF && - path != null && - activeUser!!.username != null - ) { - return ApiUtils.getUrlForFileDownload( - activeUser!!.baseUrl!!, - activeUser!!.username!!, - path - ) + if (path != null && activeUser!!.username != null) { + if (shouldAutoplayGif()) { + return ApiUtils.getUrlForFileDownload( + activeUser!!.baseUrl!!, + activeUser!!.username!!, + path + ) + } } return ApiUtils.getUrlForFilePreviewWithFileId( activeUser!!.baseUrl!!, diff --git a/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt b/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt index 4667069f617..1e238f22f2f 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt @@ -811,7 +811,7 @@ class ComposeChatAdapter( val imageUri = message.imageUrl val mimetype = message.selectedIndividualHashMap!![KEY_MIMETYPE] val drawableResourceId = getDrawableResourceIdForMimeType(mimetype) - val isGif = mimetype == Mimetype.IMAGE_GIF + val isGif = message.shouldAutoplayGif() val authHeader = if (isGif) { ApiUtils.getCredentials(currentUser.username, currentUser.token) } else { diff --git a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt index 67b2e9a4a40..ca614ce9357 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/CapabilitiesUtil.kt @@ -131,6 +131,21 @@ object CapabilitiesUtil { return DEFAULT_CHAT_SIZE } + fun getMaxGifSize(spreedCapabilities: SpreedCapability): Long { + if (spreedCapabilities.config?.containsKey("previews") == true) { + val previewsConfigHashMap = spreedCapabilities.config!!["previews"] + if (previewsConfigHashMap?.containsKey("max-gif-size") == true) { + val maxGifSize = previewsConfigHashMap["max-gif-size"]!!.toString().toLong() + return if (maxGifSize > 0) { + maxGifSize + } else { + DEFAULT_MAX_GIF_SIZE + } + } + } + return DEFAULT_MAX_GIF_SIZE + } + fun conversationDescriptionLength(spreedCapabilities: SpreedCapability): Int { if (spreedCapabilities.config?.containsKey("conversations") == true) { val map: Map? = spreedCapabilities.config!!["conversations"] @@ -331,6 +346,7 @@ object CapabilitiesUtil { private val TAG = CapabilitiesUtil::class.java.simpleName const val DEFAULT_CHAT_SIZE = 1000 + const val DEFAULT_MAX_GIF_SIZE = 3_145_728L const val RECORDING_CONSENT_NOT_REQUIRED = 0 const val RECORDING_CONSENT_REQUIRED = 1 const val RECORDING_CONSENT_DEPEND_ON_CONVERSATION = 2 From bf9f3745ef10441bdb14fa418d983c487312657b Mon Sep 17 00:00:00 2001 From: Jens Zalzala Date: Mon, 23 Mar 2026 08:33:03 -0500 Subject: [PATCH 3/3] Updated shouldAutoplayGif to not have more than 2 return statements. Removed unused import statements Signed-off-by: Jens Zalzala --- .../adapters/messages/PreviewMessageViewHolder.kt | 3 --- .../nextcloud/talk/chat/data/model/ChatMessage.kt | 15 +++++++++------ .../com/nextcloud/talk/ui/ComposeChatAdapter.kt | 1 - 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt index 318ee7cd6d2..f0ccd02c0f8 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/PreviewMessageViewHolder.kt @@ -17,7 +17,6 @@ import android.os.Handler import android.util.Base64 import android.util.Log import android.view.View -import android.view.ViewGroup import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView @@ -50,7 +49,6 @@ import com.nextcloud.talk.utils.message.MessageUtils import com.stfalcon.chatkit.messages.MessageHolders.IncomingImageMessageViewHolder import coil.load import com.nextcloud.talk.utils.ApiUtils -import com.nextcloud.talk.utils.MimetypeUtils import io.reactivex.Single import io.reactivex.SingleObserver import io.reactivex.disposables.Disposable @@ -109,7 +107,6 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : messageText.visibility = View.VISIBLE // Check if image is GIF and load animated image - val mimetype = message.selectedIndividualHashMap?.get(KEY_MIMETYPE) if (message.imageUrl != null && message.shouldAutoplayGif()) { image.adjustViewBounds = true image.load(message.imageUrl) { diff --git a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt index 937c89ba760..c1691a3fc95 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/data/model/ChatMessage.kt @@ -242,12 +242,15 @@ data class ChatMessage( */ fun shouldAutoplayGif(): Boolean { val mimetype = selectedIndividualHashMap?.get("mimetype") - if (mimetype != Mimetype.IMAGE_GIF) return false - val user = activeUser ?: return false - val capabilities = user.capabilities?.spreedCapability ?: return false - val maxGifSize = CapabilitiesUtil.getMaxGifSize(capabilities) - val fileSize = selectedIndividualHashMap?.get("size")?.toLongOrNull() ?: return true - return fileSize in 1..maxGifSize + val capabilities = activeUser?.capabilities?.spreedCapability + val fileSize = selectedIndividualHashMap?.get("size")?.toLongOrNull() + + return if (mimetype != Mimetype.IMAGE_GIF || activeUser == null || capabilities == null) { + false + } else { + val maxGifSize = CapabilitiesUtil.getMaxGifSize(capabilities) + fileSize == null || fileSize in 1..maxGifSize + } } @Suppress("Detekt.NestedBlockDepth") diff --git a/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt b/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt index 1e238f22f2f..d3671396dfc 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt @@ -107,7 +107,6 @@ import com.nextcloud.talk.ui.theme.ViewThemeUtils import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DateUtils -import com.nextcloud.talk.utils.Mimetype import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType import com.nextcloud.talk.utils.message.MessageUtils