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..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 @@ -47,6 +47,8 @@ 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 io.reactivex.Single import io.reactivex.SingleObserver import io.reactivex.disposables.Disposable @@ -100,6 +102,22 @@ 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 + if (message.imageUrl != null && message.shouldAutoplayGif()) { + 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 +128,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 +135,11 @@ abstract class PreviewMessageViewHolder(itemView: View?, payload: Any?) : messageText.text = fileName + // hide filename display for GIF images + if (message.shouldAutoplayGif()) { + 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..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 @@ -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 @@ -236,6 +237,22 @@ 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") + 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") override fun getImageUrl(): String? { if (messageParameters != null && messageParameters!!.size > 0) { @@ -246,6 +263,16 @@ data class ChatMessage( selectedIndividualHashMap = individualHashMap if (!isVoiceMessage) { if (activeUser != null && activeUser!!.baseUrl != null) { + val path = individualHashMap["path"] + if (path != null && activeUser!!.username != null) { + if (shouldAutoplayGif()) { + 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..d3671396dfc 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/ComposeChatAdapter.kt @@ -105,6 +105,7 @@ 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.DisplayUtils import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType @@ -809,7 +810,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 = message.shouldAutoplayGif() + 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, 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