From 9a315f2bd00997f212fd96d65cf2821515fdd9cf Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 18 Aug 2022 12:53:29 +0200 Subject: [PATCH 01/69] [3369] Implement core changes in order to enable video thumbnail --- .../api/stream-chat-android-client.api | 14 ++ .../chat/android/client/ChatClient.kt | 3 +- .../chat/android/client/api/ChatApi.kt | 3 +- .../client/api/models/UploadFileResponse.kt | 5 +- .../chat/android/client/api2/MoshiChatApi.kt | 3 +- .../api2/mapping/UploadFileResponseMapping.kt | 26 +++ .../android/client/channel/ChannelClient.kt | 3 +- .../android/client/models/UploadedFile.kt | 29 +++ .../android/client/uploader/FileUploader.kt | 5 +- .../client/uploader/StreamFileUploader.kt | 14 +- .../java/client/helpers/MyFileUploader.java | 9 +- .../kotlin/client/helpers/MyFileUploader.kt | 9 +- .../internal/AttachmentUploader.kt | 181 ++++++++++++++++-- 13 files changed, 271 insertions(+), 33 deletions(-) create mode 100644 stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/UploadFileResponseMapping.kt create mode 100644 stream-chat-android-client/src/main/java/io/getstream/chat/android/client/models/UploadedFile.kt diff --git a/stream-chat-android-client/api/stream-chat-android-client.api b/stream-chat-android-client/api/stream-chat-android-client.api index 9889cc3f9d3..461e9d0769d 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -3185,6 +3185,20 @@ public final class io/getstream/chat/android/client/models/TypingEvent { public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/client/models/UploadedFile { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/chat/android/client/models/UploadedFile; + public static synthetic fun copy$default (Lio/getstream/chat/android/client/models/UploadedFile;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/chat/android/client/models/UploadedFile; + public fun equals (Ljava/lang/Object;)Z + public final fun getFile ()Ljava/lang/String; + public final fun getThumbUrl ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/client/models/User : io/getstream/chat/android/client/api/models/querysort/ComparableFieldProvider, io/getstream/chat/android/client/models/CustomObject { public fun ()V public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZLjava/util/List;ZLjava/util/Date;Ljava/util/Date;Ljava/util/Date;IILjava/util/List;Ljava/util/List;Ljava/util/List;Ljava/util/Map;)V diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt index 480567a46cc..0edb0d84536 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt @@ -104,6 +104,7 @@ import io.getstream.chat.android.client.models.Mute import io.getstream.chat.android.client.models.PushMessage import io.getstream.chat.android.client.models.Reaction import io.getstream.chat.android.client.models.SearchMessagesResult +import io.getstream.chat.android.client.models.UploadedFile import io.getstream.chat.android.client.models.User import io.getstream.chat.android.client.notifications.ChatNotifications import io.getstream.chat.android.client.notifications.PushNotificationReceivedListener @@ -698,7 +699,7 @@ internal constructor( channelId: String, file: File, callback: ProgressCallback? = null, - ): Call { + ): Call { return api.sendFile(channelType, channelId, file, callback) } diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt index 8bc55fd933b..84d566aa9d7 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt @@ -39,6 +39,7 @@ import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.client.models.Mute import io.getstream.chat.android.client.models.Reaction import io.getstream.chat.android.client.models.SearchMessagesResult +import io.getstream.chat.android.client.models.UploadedFile import io.getstream.chat.android.client.models.User import io.getstream.chat.android.client.utils.ProgressCallback import java.io.File @@ -52,7 +53,7 @@ internal interface ChatApi { fun appSettings(): Call @CheckResult - fun sendFile(channelType: String, channelId: String, file: File, callback: ProgressCallback? = null): Call + fun sendFile(channelType: String, channelId: String, file: File, callback: ProgressCallback? = null): Call @CheckResult fun sendImage(channelType: String, channelId: String, file: File, callback: ProgressCallback? = null): Call diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/models/UploadFileResponse.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/models/UploadFileResponse.kt index b62a0dc3d57..899f1fab9f9 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/models/UploadFileResponse.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/models/UploadFileResponse.kt @@ -19,4 +19,7 @@ package io.getstream.chat.android.client.api.models import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -internal data class UploadFileResponse(val file: String) +internal data class UploadFileResponse( + val file: String, + val thumb_url: String?, +) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt index 23492657700..5466ba20168 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt @@ -92,6 +92,7 @@ import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.client.models.Mute import io.getstream.chat.android.client.models.Reaction import io.getstream.chat.android.client.models.SearchMessagesResult +import io.getstream.chat.android.client.models.UploadedFile import io.getstream.chat.android.client.models.User import io.getstream.chat.android.client.parser.toMap import io.getstream.chat.android.client.uploader.FileUploader @@ -312,7 +313,7 @@ constructor( channelId: String, file: File, callback: ProgressCallback?, - ): Call { + ): Call { return CoroutineCall(coroutineScope) { if (callback != null) { fileUploader.sendFile( diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/UploadFileResponseMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/UploadFileResponseMapping.kt new file mode 100644 index 00000000000..5af7c7a0b46 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/UploadFileResponseMapping.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.client.api2.mapping + +import io.getstream.chat.android.client.api.models.UploadFileResponse +import io.getstream.chat.android.client.models.UploadedFile + +internal fun UploadFileResponse.toUploadedFile() = + UploadedFile( + file = this.file, + thumbUrl = this.thumb_url + ) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt index ac762d5ef25..5d61e77f108 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt @@ -85,6 +85,7 @@ import io.getstream.chat.android.client.models.Member import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.client.models.Mute import io.getstream.chat.android.client.models.Reaction +import io.getstream.chat.android.client.models.UploadedFile import io.getstream.chat.android.client.uploader.FileUploader import io.getstream.chat.android.client.uploader.StreamCdnImageMimeTypes import io.getstream.chat.android.client.utils.ProgressCallback @@ -425,7 +426,7 @@ public class ChannelClient internal constructor( */ @CheckResult @JvmOverloads - public fun sendFile(file: File, callback: ProgressCallback? = null): Call { + public fun sendFile(file: File, callback: ProgressCallback? = null): Call { return client.sendFile(channelType, channelId, file, callback) } diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/models/UploadedFile.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/models/UploadedFile.kt new file mode 100644 index 00000000000..c822c47c268 --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/models/UploadedFile.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.client.models + +/** + * Represents a successfully uploaded file. + * + * @param file The URL of the uploaded file. + * @param thumbUrl The URL of the thumbnail of the uploaded file. + * This property is usually reserved for video files. + */ +public data class UploadedFile( + val file: String, + val thumbUrl: String? = null, +) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/uploader/FileUploader.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/uploader/FileUploader.kt index 9785c9f8d33..0026900dd4a 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/uploader/FileUploader.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/uploader/FileUploader.kt @@ -16,6 +16,7 @@ package io.getstream.chat.android.client.uploader +import io.getstream.chat.android.client.models.UploadedFile import io.getstream.chat.android.client.utils.ProgressCallback import io.getstream.chat.android.client.utils.Result import java.io.File @@ -42,7 +43,7 @@ public interface FileUploader { connectionId: String, file: File, callback: ProgressCallback, - ): Result + ): Result /** * Uploads a file for the given channel. @@ -59,7 +60,7 @@ public interface FileUploader { userId: String, connectionId: String, file: File, - ): Result + ): Result /** * Uploads an image for the given channel. Progress can be accessed via [callback]. diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/uploader/StreamFileUploader.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/uploader/StreamFileUploader.kt index 2a4b7fe6946..c0c2327a43a 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/uploader/StreamFileUploader.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/uploader/StreamFileUploader.kt @@ -17,7 +17,9 @@ package io.getstream.chat.android.client.uploader import io.getstream.chat.android.client.api.RetrofitCdnApi +import io.getstream.chat.android.client.api2.mapping.toUploadedFile import io.getstream.chat.android.client.extensions.getMediaType +import io.getstream.chat.android.client.models.UploadedFile import io.getstream.chat.android.client.utils.ProgressCallback import io.getstream.chat.android.client.utils.Result import io.getstream.chat.android.client.utils.map @@ -37,7 +39,7 @@ internal class StreamFileUploader( connectionId: String, file: File, callback: ProgressCallback, - ): Result { + ): Result { val body = file.asRequestBody(file.getMediaType()) val part = MultipartBody.Part.createFormData("file", file.name, body) @@ -47,7 +49,9 @@ internal class StreamFileUploader( file = part, connectionId = connectionId, progressCallback = callback, - ).execute().map { it.file } + ).execute().map { + it.toUploadedFile() + } } override fun sendFile( @@ -56,7 +60,7 @@ internal class StreamFileUploader( userId: String, connectionId: String, file: File, - ): Result { + ): Result { val body = file.asRequestBody(file.getMediaType()) val part = MultipartBody.Part.createFormData("file", file.name, body) @@ -66,7 +70,9 @@ internal class StreamFileUploader( file = part, connectionId = connectionId, progressCallback = null, - ).execute().map { it.file } + ).execute().map { + it.toUploadedFile() + } } override fun sendImage( diff --git a/stream-chat-android-docs/src/main/java/io/getstream/chat/docs/java/client/helpers/MyFileUploader.java b/stream-chat-android-docs/src/main/java/io/getstream/chat/docs/java/client/helpers/MyFileUploader.java index d7c6e7c1ef2..0a70b60eed2 100644 --- a/stream-chat-android-docs/src/main/java/io/getstream/chat/docs/java/client/helpers/MyFileUploader.java +++ b/stream-chat-android-docs/src/main/java/io/getstream/chat/docs/java/client/helpers/MyFileUploader.java @@ -6,6 +6,7 @@ import java.io.File; +import io.getstream.chat.android.client.models.UploadedFile; import io.getstream.chat.android.client.uploader.FileUploader; import io.getstream.chat.android.client.utils.ProgressCallback; import io.getstream.chat.android.client.utils.Result; @@ -14,9 +15,9 @@ public class MyFileUploader implements FileUploader { @Nullable @Override - public Result sendFile(@NotNull String channelType, @NotNull String channelId, @NotNull String userId, @NotNull String connectionId, @NotNull File file, @NotNull ProgressCallback callback) { + public Result sendFile(@NotNull String channelType, @NotNull String channelId, @NotNull String userId, @NotNull String connectionId, @NotNull File file, @NotNull ProgressCallback callback) { try { - return Result.success("url"); + return Result.success(new UploadedFile("file url", "thumb url")); } catch (Exception e) { return Result.error(e); } @@ -24,9 +25,9 @@ public Result sendFile(@NotNull String channelType, @NotNull String chan @Nullable @Override - public Result sendFile(@NotNull String channelType, @NotNull String channelId, @NotNull String userId, @NotNull String connectionId, @NotNull File file) { + public Result sendFile(@NotNull String channelType, @NotNull String channelId, @NotNull String userId, @NotNull String connectionId, @NotNull File file) { try { - return Result.success("url"); + return Result.success(new UploadedFile("file url", "thumb url")); } catch (Exception e) { return Result.error(e); } diff --git a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/client/helpers/MyFileUploader.kt b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/client/helpers/MyFileUploader.kt index 139a984c64f..c8b43bd69ca 100644 --- a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/client/helpers/MyFileUploader.kt +++ b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/client/helpers/MyFileUploader.kt @@ -1,5 +1,6 @@ package io.getstream.chat.docs.kotlin.client.helpers +import io.getstream.chat.android.client.models.UploadedFile import io.getstream.chat.android.client.uploader.FileUploader import io.getstream.chat.android.client.utils.ProgressCallback import io.getstream.chat.android.client.utils.Result @@ -13,9 +14,9 @@ class MyFileUploader : FileUploader { connectionId: String, file: File, callback: ProgressCallback, - ): Result { + ): Result { return try { - Result.success("url") + Result.success(UploadedFile("file url", "thumb url")) } catch (e: Exception) { Result.error(e) } @@ -27,9 +28,9 @@ class MyFileUploader : FileUploader { userId: String, connectionId: String, file: File, - ): Result { + ): Result { return try { - Result.success("url") + Result.success(UploadedFile("file url", "thumb url")) } catch (e: Exception) { Result.error(e) } diff --git a/stream-chat-android-state/src/main/java/io/getstream/chat/android/offline/message/attachments/internal/AttachmentUploader.kt b/stream-chat-android-state/src/main/java/io/getstream/chat/android/offline/message/attachments/internal/AttachmentUploader.kt index c1b96dd57ce..f9aad83abd6 100644 --- a/stream-chat-android-state/src/main/java/io/getstream/chat/android/offline/message/attachments/internal/AttachmentUploader.kt +++ b/stream-chat-android-state/src/main/java/io/getstream/chat/android/offline/message/attachments/internal/AttachmentUploader.kt @@ -28,6 +28,17 @@ internal class AttachmentUploader( private val client: ChatClient = ChatClient.instance(), ) { + /** + * Uploads the given attachment. + * + * @param channelType The type of the channel. + * @param channelId The ID of the channel. + * @param attachment The attachment to be uploaded. + * @param progressCallback Used to listen to file upload + * progress, success, and failure. + * + * @return The resulting uploaded attachment. + */ internal suspend fun uploadAttachment( channelType: String, channelId: String, @@ -36,34 +47,172 @@ internal class AttachmentUploader( ): Result { val file = checkNotNull(attachment.upload) { "An attachment needs to have a non null attachment.upload value" } - val mimeType: String? = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension) - ?: attachment.mimeType + val mimeType: String = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension) + ?: attachment.mimeType ?: "" val attachmentType = mimeType.toAttachmentType() - val result = if (attachmentType == AttachmentType.IMAGE) { - val call = client.sendImage(channelType, channelId, file, progressCallback) - call.await() + return if (attachmentType == AttachmentType.IMAGE) { + uploadImage( + channelType = channelType, + channelId = channelId, + file = file, + progressCallback = progressCallback, + attachment = attachment, + mimeType = mimeType, + attachmentType = attachmentType + ) } else { - val call = client.sendFile(channelType, channelId, file, progressCallback) - call.await() + uploadFile( + channelType = channelType, + channelId = channelId, + file = file, + progressCallback = progressCallback, + attachment = attachment, + mimeType = mimeType, + attachmentType = attachmentType + ) } + } + + /** + * Uploads an image attachment. + * + * @param channelType The type of the channel. + * @param channelId The ID of the channel. + * @param file The file that will be uploaded. + * @param attachment The attachment to be uploaded. + * @param progressCallback Used to listen to file upload + * progress, success, and failure. + * @param mimeType The mime type of the attachment that will be uploaded, + * e.g. image/jpeg. + * @param attachmentType The type of the attachment, e.g. "video", "audio", etc. + * + * @return The resulting uploaded attachment. + */ + private suspend fun uploadImage( + channelType: String, + channelId: String, + file: File, + progressCallback: ProgressCallback?, + attachment: Attachment, + mimeType: String, + attachmentType: AttachmentType, + ): Result { + val result = client.sendImage(channelType, channelId, file, progressCallback) + .await() + return if (result.isSuccess) { val augmentedAttachment = attachment.augmentAttachmentOnSuccess( file = file, - mimeType = mimeType ?: "", + mimeType = mimeType, attachmentType = attachmentType, url = result.data() ) - augmentedAttachment.uploadState = Attachment.UploadState.Success - progressCallback?.onSuccess(augmentedAttachment.url) - Result(augmentedAttachment) + + onSuccessfulUpload( + augmentedAttachment = augmentedAttachment, + progressCallback = progressCallback + ) + } else { + onFailedUpload( + attachment = attachment, + result = result, + progressCallback = progressCallback + ) + } + } + + /** + * Uploads an image attachment. + * + * @param channelType The type of the channel. + * @param channelId The ID of the channel. + * @param file The file that will be uploaded. + * @param attachment The attachment to be uploaded. + * @param progressCallback Used to listen to file upload + * progress, success, and failure. + * @param mimeType The mime type of the attachment that will be uploaded, + * e.g. image/jpeg. + * @param attachmentType The type of the attachment, e.g. "video", "audio", etc. + * + * @return The resulting uploaded attachment. + */ + private suspend fun uploadFile( + channelType: String, + channelId: String, + file: File, + progressCallback: ProgressCallback?, + attachment: Attachment, + mimeType: String, + attachmentType: AttachmentType, + ): Result { + val result = client.sendFile(channelType, channelId, file, progressCallback) + .await() + + return if (result.isSuccess) { + val augmentedAttachment = attachment.augmentAttachmentOnSuccess( + file = file, + mimeType = mimeType, + attachmentType = attachmentType, + url = result.data().file, + thumbUrl = result.data().thumbUrl + ) + + onSuccessfulUpload( + augmentedAttachment = augmentedAttachment, + progressCallback = progressCallback + ) } else { - attachment.uploadState = Attachment.UploadState.Failed(result.error()) - progressCallback?.onError(result.error()) - Result(result.error()) + onFailedUpload( + attachment = attachment, + result = result, + progressCallback = progressCallback + ) } } + /** + * Updates the upload state and calls the appropriate [ProgressCallback] + * method. + * + * @param augmentedAttachment The attachment pre filled with + * the appropriate fields after the file contained in the attachment + * was uploaded. + * @param progressCallback Used to listen to file upload + * progress, success, and failure. + * + * @return The resulting successfully uploaded attachment. + * */ + private fun onSuccessfulUpload( + augmentedAttachment: Attachment, + progressCallback: ProgressCallback?, + ): Result { + augmentedAttachment.uploadState = Attachment.UploadState.Success + progressCallback?.onSuccess(augmentedAttachment.url) + return Result(augmentedAttachment) + } + + /** + * Updates the upload state and calls the appropriate [ProgressCallback] + * method. + * + * @param attachment The attachment that has failed to upload. + * @param result The result of the failed upload. + * @param progressCallback Used to listen to file upload + * progress, success, and failure. + * + * @return Returns a [Result] containing a [io.getstream.chat.android.client.errors.ChatError] + * */ + private fun onFailedUpload( + attachment: Attachment, + result: Result, + progressCallback: ProgressCallback?, + ): Result { + attachment.uploadState = Attachment.UploadState.Failed(result.error()) + progressCallback?.onError(result.error()) + return Result(result.error()) + } + /** * Augment an attachment instance with data from uploaded file, mimeType, attachmentType and obtained from backend * url. @@ -72,12 +221,15 @@ internal class AttachmentUploader( * @param mimeType MimeType of uploaded attachment. * @param attachmentType File, video or picture enum instance. * @param url URL obtained from BE. + * @param thumbUrl The thumbnail obtained from the BE. + * Usually returned for uploaded videos, can be null otherwise. */ private fun Attachment.augmentAttachmentOnSuccess( file: File, mimeType: String, attachmentType: AttachmentType, url: String, + thumbUrl: String? = null, ): Attachment { return copy( name = file.name, @@ -99,6 +251,7 @@ internal class AttachmentUploader( if (title.isNullOrBlank()) { title = file.name } + this.thumbUrl = thumbUrl } } From d6ce67a1fb3719fcc4a35a597b91c179cbefb56f Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 18 Aug 2022 13:00:29 +0200 Subject: [PATCH 02/69] [3369] Appease Detekt. --- .../java/io/getstream/chat/android/client/api/ChatApi.kt | 7 ++++++- stream-chat-android-state/detekt-baseline.xml | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt index 84d566aa9d7..58de2ac5744 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt @@ -53,7 +53,12 @@ internal interface ChatApi { fun appSettings(): Call @CheckResult - fun sendFile(channelType: String, channelId: String, file: File, callback: ProgressCallback? = null): Call + fun sendFile( + channelType: String, + channelId: String, + file: File, + callback: ProgressCallback? = null, + ): Call @CheckResult fun sendImage(channelType: String, channelId: String, file: File, callback: ProgressCallback? = null): Call diff --git a/stream-chat-android-state/detekt-baseline.xml b/stream-chat-android-state/detekt-baseline.xml index 33d00e9ff08..21970d82729 100644 --- a/stream-chat-android-state/detekt-baseline.xml +++ b/stream-chat-android-state/detekt-baseline.xml @@ -33,6 +33,7 @@ LongMethod:EventHandlerImpl.kt$EventHandlerImpl$private suspend fun updateOfflineStorageFromEvents(events: List<ChatEvent>, isFromSync: Boolean) LongMethod:EventHandlerSequential.kt$EventHandlerSequential$private suspend fun updateOfflineStorage(batchEvent: BatchEvent) LongMethod:QueryChannelsSortTest.kt$QueryChannelsSortTest.Companion$@JvmStatic fun hasUnreadSortArguments() + LongParameterList:AttachmentUploader.kt$AttachmentUploader$( channelType: String, channelId: String, file: File, progressCallback: ProgressCallback?, attachment: Attachment, mimeType: String, attachmentType: AttachmentType, ) MagicNumber:QueryChannelPaginationRequest.kt$QueryChannelPaginationRequest$30 MagicNumber:SyncManager.kt$SyncManager$30 MatchingDeclarationName:OfflineErrorHandlerFactories.kt$OfflineErrorHandlerFactoriesProvider From 06f14cade7903d064fe95b22d3bea583cff4b81b Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 18 Aug 2022 13:36:05 +0200 Subject: [PATCH 03/69] [3369] Updated the changelog. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5ac68f50b..e892c429edf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,13 @@ ### ⬆️ Improved ### ✅ Added +- Added `UploadedFile` which represents an uploaded file. It contains the url to the file under the property `file`, and a thumbnail of the file under the property `thumbUrl`. Thumbnails are usually returned when uploading a video file. [#4058](https://github.com/GetStream/stream-chat-android/pull/4058) ### ⚠️ Changed +- `ChatClient.sendFile` now returns `UploadedFile` instead of `String`. `UploadedFile.file` is the equivalent of the previous return value. If you do not need the other parts of `UploadedFile`, you can use `.map { it.file }` to mitigate the breaking change. [#4058](https://github.com/GetStream/stream-chat-android/pull/4058) +- `ChannelClient.sendFile` now returns `UploadedFile` instead of `String`. `UploadedFile.file` is the equivalent of the previous return value. If you do not need the other parts of `UploadedFile`, you can use `.map { it.file }` to mitigate the breaking change. [#4058](https://github.com/GetStream/stream-chat-android/pull/4058) +- Overloaded functions `FileUploader.sendFile()` have their signatures changed. Instead of returning `String`, they are now supposed to return `UploadedFile`. If you have extended this interface and only need the previous return functionality, you can assign a value to `UploadedFile.file` while keeping `UploadedFile.thumbUrl` `null`. [#4058](https://github.com/GetStream/stream-chat-android/pull/4058) +- Overloaded functions `StreamFileUploader.sendFile()` have their signatures changed. Instead of returning `String`, they now return `UploadedFile`. If you do not need the other parts of `UploadedFile`, you can use `.map { it.file }` to mitigate the breaking change. [#4058](https://github.com/GetStream/stream-chat-android/pull/4058) ### ❌ Removed From 22c8afe3ffb56aac4bfb9c46b3513edb3791dbc2 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 18 Aug 2022 15:42:15 +0200 Subject: [PATCH 04/69] [3369] Update tests. --- .../client/uploader/StreamFileUploaderTest.kt | 19 ++++++++++------- .../attachment/AttachmentUploaderTests.kt | 21 ++++++++++++------- .../UploadAttachmentsIntegrationTests.kt | 17 +++++++++------ 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/uploader/StreamFileUploaderTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/uploader/StreamFileUploaderTest.kt index 01f1bceb606..42a501ac7c5 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/uploader/StreamFileUploaderTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/uploader/StreamFileUploaderTest.kt @@ -22,6 +22,7 @@ import io.getstream.chat.android.client.api.RetrofitCdnApi import io.getstream.chat.android.client.api.models.CompletableResponse import io.getstream.chat.android.client.api.models.UploadFileResponse import io.getstream.chat.android.client.errors.ChatError +import io.getstream.chat.android.client.models.UploadedFile import io.getstream.chat.android.client.utils.ProgressCallback import io.getstream.chat.android.client.utils.RetroError import io.getstream.chat.android.client.utils.RetroSuccess @@ -69,7 +70,7 @@ internal class StreamFileUploaderTest { @Test fun `Should send file to api when sending file without progress callback`() { whenever(retrofitCdnApi.sendFile(any(), any(), any(), any(), anyOrNull())).thenReturn( - RetroSuccess(UploadFileResponse("file")).toRetrofitCall() + RetroSuccess(UploadFileResponse(file = "file", thumb_url = "thumb_url")).toRetrofitCall() ) streamFileUploader.sendFile(channelType, channelId, userId, connectionId, File("")) @@ -86,13 +87,15 @@ internal class StreamFileUploaderTest { @Test fun `Should return result containing file when successfully sent file without progress callback`() { val file = "file" + val thumbUrl = "thumb_url" + whenever(retrofitCdnApi.sendFile(any(), any(), any(), any(), anyOrNull())).thenReturn( - RetroSuccess(UploadFileResponse(file)).toRetrofitCall() + RetroSuccess(UploadFileResponse(file = file, thumb_url = thumbUrl)).toRetrofitCall() ) val result = streamFileUploader.sendFile(channelType, channelId, userId, connectionId, File("")) - result.data() shouldBeEqualTo file + result.data() shouldBeEqualTo UploadedFile(file = file, thumbUrl = thumbUrl) } @Test @@ -109,7 +112,7 @@ internal class StreamFileUploaderTest { @Test fun `Should send file to api when sending file with progress callback`() { whenever(retrofitCdnApi.sendFile(any(), any(), any(), any(), anyOrNull())).thenReturn( - RetroSuccess(UploadFileResponse("file")).toRetrofitCall() + RetroSuccess(UploadFileResponse(file = "file", thumb_url = "thumb_url")).toRetrofitCall() ) streamFileUploader.sendFile( @@ -133,7 +136,7 @@ internal class StreamFileUploaderTest { @Test fun `Should send image to api when sending image without progress callback`() { whenever(retrofitCdnApi.sendImage(any(), any(), any(), any(), anyOrNull())).thenReturn( - RetroSuccess(UploadFileResponse("file")).toRetrofitCall() + RetroSuccess(UploadFileResponse(file = "file", thumb_url = "thumb_url")).toRetrofitCall() ) streamFileUploader.sendImage(channelType, channelId, userId, connectionId, File("")) @@ -150,8 +153,10 @@ internal class StreamFileUploaderTest { @Test fun `Should return result containing file when successfully sent image without progress callback`() { val file = "file" + val thumbUrl: String? = null + whenever(retrofitCdnApi.sendImage(any(), any(), any(), any(), anyOrNull())).thenReturn( - RetroSuccess(UploadFileResponse(file)).toRetrofitCall() + RetroSuccess(UploadFileResponse(file = file, thumb_url = thumbUrl)).toRetrofitCall() ) val result = streamFileUploader.sendImage(channelType, channelId, userId, connectionId, File("")) @@ -173,7 +178,7 @@ internal class StreamFileUploaderTest { @Test fun `Should send image to api when sending image with progress callback`() { whenever(retrofitCdnApi.sendImage(any(), any(), any(), any(), anyOrNull())).thenReturn( - RetroSuccess(UploadFileResponse("file")).toRetrofitCall() + RetroSuccess(UploadFileResponse(file = "file", thumb_url = "thumb_url")).toRetrofitCall() ) streamFileUploader.sendImage( diff --git a/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/AttachmentUploaderTests.kt b/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/AttachmentUploaderTests.kt index f356a6d93e1..b63f39b18a2 100644 --- a/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/AttachmentUploaderTests.kt +++ b/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/AttachmentUploaderTests.kt @@ -22,6 +22,7 @@ import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.errors.ChatError import io.getstream.chat.android.client.extensions.uploadId import io.getstream.chat.android.client.models.Attachment +import io.getstream.chat.android.client.models.UploadedFile import io.getstream.chat.android.client.utils.Result import io.getstream.chat.android.offline.message.attachments.internal.AttachmentUploader import io.getstream.chat.android.test.TestCall @@ -91,7 +92,7 @@ internal class AttachmentUploaderTests { val url = "url" val sut = Fixture() - .givenMockedFileUploads(channelType, channelId, Result(url)) + .givenMockedFileUploads(channelType, channelId, Result(UploadedFile(file = url))) .get() val result = sut.uploadAttachment(channelType, channelId, attachment) @@ -174,17 +175,21 @@ internal class AttachmentUploaderTests { private class Fixture { private var clientMock: ChatClient = mock() - fun givenMockedFileUploads(channelType: String, channelId: String, result: Result) = apply { + fun givenMockedFileUploads(channelType: String, channelId: String, result: Result) = apply { whenever( - clientMock.sendImage( + clientMock.sendFile( eq(channelType), eq(channelId), any(), anyOrNull() ) ) doReturn TestCall(result) + + } + + fun givenMockedImageUploads(channelType: String, channelId: String, result: Result) { whenever( - clientMock.sendFile( + clientMock.sendImage( eq(channelType), eq(channelId), any(), @@ -195,7 +200,9 @@ internal class AttachmentUploaderTests { fun givenMockedFileUploads(channelType: String, channelId: String, files: List) = apply { for (file in files) { - val result = Result(file.absolutePath) + val imageResult = Result(file.absolutePath) + val fileResult = Result(UploadedFile(file = file.absolutePath)) + whenever( clientMock.sendFile( eq(channelType), @@ -203,7 +210,7 @@ internal class AttachmentUploaderTests { same(file), anyOrNull(), ) - ) doReturn TestCall(result) + ) doReturn TestCall(fileResult) whenever( clientMock.sendImage( eq(channelType), @@ -211,7 +218,7 @@ internal class AttachmentUploaderTests { same(file), anyOrNull(), ) - ) doReturn TestCall(result) + ) doReturn TestCall(imageResult) } } diff --git a/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/UploadAttachmentsIntegrationTests.kt b/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/UploadAttachmentsIntegrationTests.kt index bc1416d01e2..584d5f3c8e8 100644 --- a/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/UploadAttachmentsIntegrationTests.kt +++ b/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/UploadAttachmentsIntegrationTests.kt @@ -24,6 +24,7 @@ import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.errors.ChatError import io.getstream.chat.android.client.models.Attachment import io.getstream.chat.android.client.models.Message +import io.getstream.chat.android.client.models.UploadedFile import io.getstream.chat.android.client.persistance.repository.MessageRepository import io.getstream.chat.android.client.query.pagination.AnyChannelPaginationRequest import io.getstream.chat.android.client.test.randomMessage @@ -160,7 +161,9 @@ internal class UploadAttachmentsIntegrationTests { private fun mockFileUploadsFailure(files: List) { for (file in files) { - val result = Result(ChatError()) + val imageResult = Result(ChatError()) + val fileResult = Result(ChatError()) + whenever( chatClient.sendFile( eq(channelType), @@ -168,7 +171,7 @@ internal class UploadAttachmentsIntegrationTests { same(file), anyOrNull(), ) - ) doReturn TestCall(result) + ) doReturn TestCall(fileResult) whenever( chatClient.sendImage( eq(channelType), @@ -176,13 +179,15 @@ internal class UploadAttachmentsIntegrationTests { same(file), anyOrNull(), ) - ) doReturn TestCall(result) + ) doReturn TestCall(imageResult) } } private fun mockFileUploadsSuccess(files: List) { for (file in files) { - val result = Result("file") + val imageResult = Result("file") + val fileResult = Result(UploadedFile(file = "file", thumbUrl = "thumbUrl")) + whenever( chatClient.sendFile( eq(channelType), @@ -190,7 +195,7 @@ internal class UploadAttachmentsIntegrationTests { any(), anyOrNull(), ) - ) doReturn TestCall(result) + ) doReturn TestCall(fileResult) whenever( chatClient.sendImage( eq(channelType), @@ -198,7 +203,7 @@ internal class UploadAttachmentsIntegrationTests { any(), anyOrNull(), ) - ) doReturn TestCall(result) + ) doReturn TestCall(imageResult) } } } From a7b99a0a546d38ecf67d0b5f584f73009766e609 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 18 Aug 2022 15:42:42 +0200 Subject: [PATCH 05/69] [3369] Update tests. --- .../channel/controller/attachment/AttachmentUploaderTests.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/AttachmentUploaderTests.kt b/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/AttachmentUploaderTests.kt index b63f39b18a2..8c7eab8ff17 100644 --- a/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/AttachmentUploaderTests.kt +++ b/stream-chat-android-state/src/test/java/io/getstream/chat/android/offline/channel/controller/attachment/AttachmentUploaderTests.kt @@ -184,7 +184,6 @@ internal class AttachmentUploaderTests { anyOrNull() ) ) doReturn TestCall(result) - } fun givenMockedImageUploads(channelType: String, channelId: String, result: Result) { From a8359cf076f6c4f29d24720e885e8ffda719820d Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Fri, 19 Aug 2022 18:41:46 +0200 Subject: [PATCH 06/69] [3369] Start work on an attachment factory capable of displaying both Images and Video and its accompanying composables. --- .../api/stream-chat-android-compose.api | 17 + .../attachments/StreamAttachmentFactories.kt | 4 +- .../content/MediaAttachmentContent.kt | 337 ++++++++++++++++++ .../factory/MediaAttachmentFactory.kt | 43 +++ .../res/drawable/stream_compose_ic_play.xml | 25 ++ 5 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt create mode 100644 stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_play.xml diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 2e41707eee3..ebe05b79624 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -557,6 +557,10 @@ public final class io/getstream/chat/android/compose/ui/attachments/content/Link public static final fun LinkAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;ILandroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } +public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContentKt { + public static final fun MediaAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + public final class io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContentKt { public static final fun MessageAttachmentsContent (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } @@ -590,6 +594,15 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Comp public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function4; } +public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt; + public static field lambda-1 Lkotlin/jvm/functions/Function5; + public static field lambda-2 Lkotlin/jvm/functions/Function4; + public fun ()V + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function5; + public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function4; +} + public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$QuotedAttachmentFactoryKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$QuotedAttachmentFactoryKt; public static field lambda-1 Lkotlin/jvm/functions/Function4; @@ -620,6 +633,10 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Link public static final fun LinkAttachmentFactory (I)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; } +public final class io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactoryKt { + public static final fun MediaAttachmentFactory ()Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; +} + public final class io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactoryKt { public static final fun QuotedAttachmentFactory ()Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/StreamAttachmentFactories.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/StreamAttachmentFactories.kt index b828127cf53..de34a18d730 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/StreamAttachmentFactories.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/StreamAttachmentFactories.kt @@ -18,8 +18,8 @@ package io.getstream.chat.android.compose.ui.attachments import io.getstream.chat.android.compose.ui.attachments.factory.FileAttachmentFactory import io.getstream.chat.android.compose.ui.attachments.factory.GiphyAttachmentFactory -import io.getstream.chat.android.compose.ui.attachments.factory.ImageAttachmentFactory import io.getstream.chat.android.compose.ui.attachments.factory.LinkAttachmentFactory +import io.getstream.chat.android.compose.ui.attachments.factory.MediaAttachmentFactory import io.getstream.chat.android.compose.ui.attachments.factory.QuotedAttachmentFactory import io.getstream.chat.android.compose.ui.attachments.factory.UploadAttachmentFactory @@ -46,7 +46,7 @@ public object StreamAttachmentFactories { UploadAttachmentFactory(), LinkAttachmentFactory(linkDescriptionMaxLines), GiphyAttachmentFactory(), - ImageAttachmentFactory(), + MediaAttachmentFactory(), FileAttachmentFactory(), ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt new file mode 100644 index 00000000000..bef2416a0ef --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -0,0 +1,337 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.attachments.content + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.getstream.sdk.chat.utils.extensions.imagePreviewUrl +import io.getstream.chat.android.client.models.Attachment +import io.getstream.chat.android.client.models.Message +import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResult +import io.getstream.chat.android.compose.state.messages.attachments.AttachmentState +import io.getstream.chat.android.compose.ui.attachments.preview.ImagePreviewContract +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter +import io.getstream.chat.android.uiutils.constant.AttachmentType +import io.getstream.chat.android.uiutils.extension.hasLink + +/** + * Builds an image attachment message, which can be composed of several images or will show an upload state if we're + * currently uploading images. + * + * @param attachmentState The state of the attachment, holding the root modifier, the message + * and the onLongItemClick handler. + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +public fun MediaAttachmentContent( + attachmentState: AttachmentState, + modifier: Modifier = Modifier, +) { + val (message, onLongItemClick, onImagePreviewResult) = attachmentState + val gridSpacing = ChatTheme.dimens.attachmentsContentImageGridSpacing + + Row( + modifier + .clip(ChatTheme.shapes.attachment) + .combinedClickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = {}, + onLongClick = { onLongItemClick(message) } + ), + horizontalArrangement = Arrangement.spacedBy(gridSpacing) + ) { + val attachments = + message.attachments.filter { !it.hasLink() && it.type == AttachmentType.IMAGE || it.type == AttachmentType.VIDEO } + val attachmentCount = attachments.size + + if (attachmentCount == 1) { + val attachment = attachments.first() + + ShowSingleAttachment( + attachment = attachment, + message = message, + onImagePreviewResult = onImagePreviewResult, + onLongItemClick = onLongItemClick + ) + } else { + ShowMultipleAttachments( + attachments = attachments, + attachmentCount = attachmentCount, + gridSpacing = gridSpacing, + message = message, + onImagePreviewResult = onImagePreviewResult, + onLongItemClick = onLongItemClick + ) + } + } +} + +@Composable +internal fun ShowSingleAttachment( + attachment: Attachment, + message: Message, + onImagePreviewResult: (ImagePreviewResult?) -> Unit, + onLongItemClick: (Message) -> Unit, +) { + // Depending on the CDN, images might not contain their original dimensions + val ratio: Float? by remember(key1 = attachment.originalWidth, key2 = attachment.originalHeight) { + derivedStateOf { + val width = attachment.originalWidth?.toFloat() + val height = attachment.originalHeight?.toFloat() + + if (width != null && height != null) { + width / height + } else { + null + } + } + } + MediaAttachmentContentItem( + attachment = attachment, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(ratio ?: EqualDimensionsRatio), + message = message, + attachmentPosition = 0, + onImagePreviewResult = onImagePreviewResult, + onLongItemClick = onLongItemClick + ) +} + +@Composable +internal fun RowScope.ShowMultipleAttachments( + attachments: List, + attachmentCount: Int, + gridSpacing: Dp, + message: Message, + onImagePreviewResult: (ImagePreviewResult?) -> Unit, + onLongItemClick: (Message) -> Unit, +) { + + Column( + modifier = Modifier + .weight(1f, fill = false) + .aspectRatio(TwiceAsTallAsIsWideRatio), + verticalArrangement = Arrangement.spacedBy(gridSpacing) + ) { + for (imageIndex in 0..3 step 2) { + if (imageIndex < attachmentCount) { + MediaAttachmentContentItem( + attachment = attachments[imageIndex], + modifier = Modifier.weight(1f), + message = message, + attachmentPosition = imageIndex, + onImagePreviewResult = onImagePreviewResult, + onLongItemClick = onLongItemClick + ) + } + } + } + + Column( + modifier = Modifier + .weight(1f, fill = false) + .aspectRatio(TwiceAsTallAsIsWideRatio), + verticalArrangement = Arrangement.spacedBy(gridSpacing) + ) { + for (imageIndex in 1..4 step 2) { + if (imageIndex < attachmentCount) { + val attachment = attachments[imageIndex] + val isUploading = attachment.uploadState is Attachment.UploadState.InProgress + + if (imageIndex == 3 && attachmentCount > 4) { + Box(modifier = Modifier.weight(1f)) { + MediaAttachmentContentItem( + attachment = attachment, + message = message, + attachmentPosition = imageIndex, + onImagePreviewResult = onImagePreviewResult, + onLongItemClick = onLongItemClick + ) + + if (!isUploading) { + MediaAttachmentViewMoreOverlay( + imageCount = attachmentCount, + imageIndex = imageIndex, + modifier = Modifier.align(Alignment.Center) + ) + } + } + } else { + MediaAttachmentContentItem( + attachment = attachment, + modifier = Modifier.weight(1f), + message = message, + attachmentPosition = imageIndex, + onImagePreviewResult = onImagePreviewResult, + onLongItemClick = onLongItemClick + ) + } + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun MediaAttachmentContentItem( + message: Message, + attachmentPosition: Int, + attachment: Attachment, + onImagePreviewResult: (ImagePreviewResult?) -> Unit, + onLongItemClick: (Message) -> Unit, + modifier: Modifier = Modifier, +) { + val painter = rememberStreamImagePainter(attachment.imagePreviewUrl) + + val imagePreviewLauncher = rememberLauncherForActivityResult( + contract = ImagePreviewContract(), + onResult = { result -> onImagePreviewResult(result) } + ) + + Box( + modifier = modifier + .fillMaxWidth() + .combinedClickable( + interactionSource = MutableInteractionSource(), + indication = rememberRipple(), + onClick = { + imagePreviewLauncher.launch( + ImagePreviewContract.Input( + messageId = message.id, + initialPosition = attachmentPosition + ) + ) + }, + onLongClick = { onLongItemClick(message) } + ), + contentAlignment = Alignment.Center + ) { + Image( + modifier = modifier + .fillMaxSize(), + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop + ) + + if (attachment.type == AttachmentType.VIDEO) { + // TODO add attributes such as play button shape and size to ChatTheme + Box( + modifier = Modifier + .shadow(10.dp, shape = CircleShape) + .background(color = Color.White, shape = CircleShape) + .width(ChatTheme.dimens.attachmentsContentImageWidth / 6) + .aspectRatio(1f), + contentAlignment = Alignment.Center + ) { + Column { + Image( + modifier = Modifier + .alignBy { measured -> + -(measured.measuredWidth * 1 / 9) + }, + painter = painterResource(id = R.drawable.stream_compose_ic_play), + contentDescription = null, + ) + } + } + } + } +} + +/** + * Represents an overlay that's shown on the last image in the image attachment item gallery. + * + * @param imageCount The number of total images. + * @param imageIndex The current image index. + * @param modifier Modifier for styling. + */ +@Composable +internal fun MediaAttachmentViewMoreOverlay( + imageCount: Int, + imageIndex: Int, + modifier: Modifier = Modifier, +) { + val remainingImagesCount = imageCount - (imageIndex + 1) + + Box( + modifier = Modifier + .fillMaxSize() + .background(color = ChatTheme.colors.overlay), + ) { + Text( + modifier = modifier + .wrapContentSize(), + text = stringResource( + id = R.string.stream_compose_remaining_images_count, + remainingImagesCount + ), + color = ChatTheme.colors.barsBackground, + style = ChatTheme.typography.title1, + textAlign = TextAlign.Center + ) + } +} + +/** + * Produces the same height as the width of the + * Composable when calling [Modifier.aspectRatio]. + */ +private const val EqualDimensionsRatio = 1f + +/** + * Produces a height value that is twice the width of the + * Composable when calling [Modifier.aspectRatio]. + */ +private const val TwiceAsTallAsIsWideRatio = 0.5f diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt new file mode 100644 index 00000000000..c0dc5234050 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.attachments.factory + +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory +import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentContent +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.uiutils.constant.AttachmentType + +@Suppress("FunctionName") +public fun MediaAttachmentFactory(): AttachmentFactory = AttachmentFactory( + canHandle = { + it.none { attachment -> attachment.type != AttachmentType.IMAGE && attachment.type != AttachmentType.VIDEO } + }, + previewContent = { modifier, attachments, onAttachmentRemoved -> }, + content = @Composable { modifier, state -> + MediaAttachmentContent( + modifier = modifier + .width(ChatTheme.dimens.attachmentsContentImageWidth) + .wrapContentHeight() + .heightIn(max = ChatTheme.dimens.attachmentsContentImageMaxHeight), + attachmentState = state + ) + } +) diff --git a/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_play.xml b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_play.xml new file mode 100644 index 00000000000..efce09a1876 --- /dev/null +++ b/stream-chat-android-compose/src/main/res/drawable/stream_compose_ic_play.xml @@ -0,0 +1,25 @@ + + + + + From 5774c14d63e3578551bd53e5d4752f8573628eea Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 22 Aug 2022 16:54:15 +0200 Subject: [PATCH 07/69] [3369] Fully fles out `MediaAttachmentContent`s and the compsable it uses. --- .../api/stream-chat-android-compose.api | 20 ++- .../content/MediaAttachmentContent.kt | 126 +++++++++++++----- .../factory/MediaAttachmentFactory.kt | 40 +++--- 3 files changed, 133 insertions(+), 53 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index ebe05b79624..9f4fd3b29ce 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -518,6 +518,13 @@ public final class io/getstream/chat/android/compose/ui/attachments/StreamAttach public final fun defaultQuotedFactories ()Ljava/util/List; } +public final class io/getstream/chat/android/compose/ui/attachments/content/ComposableSingletons$MediaAttachmentContentKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/content/ComposableSingletons$MediaAttachmentContentKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; +} + public final class io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContentKt { public static final fun FileAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V public static final fun FileAttachmentImage (Lio/getstream/chat/android/client/models/Attachment;Landroidx/compose/runtime/Composer;I)V @@ -558,7 +565,7 @@ public final class io/getstream/chat/android/compose/ui/attachments/content/Link } public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContentKt { - public static final fun MediaAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public static final fun MediaAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContentKt { @@ -596,11 +603,11 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Comp public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt; - public static field lambda-1 Lkotlin/jvm/functions/Function5; - public static field lambda-2 Lkotlin/jvm/functions/Function4; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function5; public fun ()V - public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function5; - public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function5; } public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$QuotedAttachmentFactoryKt { @@ -634,7 +641,8 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Link } public final class io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactoryKt { - public static final fun MediaAttachmentFactory ()Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; + public static final fun MediaAttachmentFactory (Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; + public static synthetic fun MediaAttachmentFactory$default (Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; } public final class io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactoryKt { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index bef2416a0ef..11ab3498e4d 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -31,7 +31,6 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Text @@ -64,17 +63,20 @@ import io.getstream.chat.android.uiutils.constant.AttachmentType import io.getstream.chat.android.uiutils.extension.hasLink /** - * Builds an image attachment message, which can be composed of several images or will show an upload state if we're - * currently uploading images. + * Displays a preview of single or multiple video or attachments. * * @param attachmentState The state of the attachment, holding the root modifier, the message * and the onLongItemClick handler. + * @param modifier The modifier used for styling. + * @param playButton Represents the play button that is overlaid above video attachment + * previews. */ @OptIn(ExperimentalFoundationApi::class) @Composable public fun MediaAttachmentContent( attachmentState: AttachmentState, modifier: Modifier = Modifier, + playButton: @Composable () -> Unit = { PlayButton() }, ) { val (message, onLongItemClick, onImagePreviewResult) = attachmentState val gridSpacing = ChatTheme.dimens.attachmentsContentImageGridSpacing @@ -97,31 +99,45 @@ public fun MediaAttachmentContent( if (attachmentCount == 1) { val attachment = attachments.first() - ShowSingleAttachment( + ShowSingleMediaAttachment( attachment = attachment, message = message, onImagePreviewResult = onImagePreviewResult, - onLongItemClick = onLongItemClick + onLongItemClick = onLongItemClick, + playButton = playButton ) } else { - ShowMultipleAttachments( + ShowMultipleMediaAttachments( attachments = attachments, attachmentCount = attachmentCount, gridSpacing = gridSpacing, message = message, onImagePreviewResult = onImagePreviewResult, - onLongItemClick = onLongItemClick + onLongItemClick = onLongItemClick, + playButton = playButton ) } } } +/** + * Displays a preview of a single image or video attachment. + * + * @param attachment The attachment that is previewed. + * @param message The original message containing the attachment. + * @param onImagePreviewResult The result of the activity used for propagating + * actions such as image selection, deletion, etc. + * @param onLongItemClick Lambda that gets called when an item is long clicked. + * @param playButton Represents the play button that is overlaid above video attachment + * previews. + */ @Composable -internal fun ShowSingleAttachment( +internal fun ShowSingleMediaAttachment( attachment: Attachment, message: Message, onImagePreviewResult: (ImagePreviewResult?) -> Unit, onLongItemClick: (Message) -> Unit, + playButton: @Composable () -> Unit, ) { // Depending on the CDN, images might not contain their original dimensions val ratio: Float? by remember(key1 = attachment.originalWidth, key2 = attachment.originalHeight) { @@ -144,18 +160,33 @@ internal fun ShowSingleAttachment( message = message, attachmentPosition = 0, onImagePreviewResult = onImagePreviewResult, - onLongItemClick = onLongItemClick + onLongItemClick = onLongItemClick, + playButton = playButton ) } +/** + * Displays previews of multiple image and video attachment laid out in a grid. + * + * @param attachments The list of attachments that are to be previewed. + * @param attachmentCount The number of attachments that are to be previewed. + * @param gridSpacing Determines the spacing strategy between items. + * @param message The original message containing the attachments. + * @param onImagePreviewResult The result of the activity used for propagating + * actions such as image selection, deletion, etc. + * @param onLongItemClick Lambda that gets called when an item is long clicked. + * @param playButton Represents the play button that is overlaid above video attachment + * previews. + */ @Composable -internal fun RowScope.ShowMultipleAttachments( +internal fun RowScope.ShowMultipleMediaAttachments( attachments: List, attachmentCount: Int, gridSpacing: Dp, message: Message, onImagePreviewResult: (ImagePreviewResult?) -> Unit, onLongItemClick: (Message) -> Unit, + playButton: @Composable () -> Unit, ) { Column( @@ -172,7 +203,8 @@ internal fun RowScope.ShowMultipleAttachments( message = message, attachmentPosition = imageIndex, onImagePreviewResult = onImagePreviewResult, - onLongItemClick = onLongItemClick + onLongItemClick = onLongItemClick, + playButton = playButton ) } } @@ -196,7 +228,8 @@ internal fun RowScope.ShowMultipleAttachments( message = message, attachmentPosition = imageIndex, onImagePreviewResult = onImagePreviewResult, - onLongItemClick = onLongItemClick + onLongItemClick = onLongItemClick, + playButton = playButton ) if (!isUploading) { @@ -214,7 +247,8 @@ internal fun RowScope.ShowMultipleAttachments( message = message, attachmentPosition = imageIndex, onImagePreviewResult = onImagePreviewResult, - onLongItemClick = onLongItemClick + onLongItemClick = onLongItemClick, + playButton = playButton ) } } @@ -222,6 +256,21 @@ internal fun RowScope.ShowMultipleAttachments( } } +/** + * Displays previews of image and video attachments. + * + * @param message The original message containing the attachments. + * @param attachmentPosition The position of the attachment in the list + * of attachments. Used to remember the item position when viewing it in a separate + * activity. + * @param attachment The attachment that is previewed. + * @param onImagePreviewResult The result of the activity used for propagating + * actions such as image selection, deletion, etc. + * @param onLongItemClick Lambda that gets called when the item is long clicked. + * @param modifier Modifier used for styling. + * @param playButton Represents the play button that is overlaid above video attachment + * previews. + */ @OptIn(ExperimentalFoundationApi::class) @Composable internal fun MediaAttachmentContentItem( @@ -231,6 +280,7 @@ internal fun MediaAttachmentContentItem( onImagePreviewResult: (ImagePreviewResult?) -> Unit, onLongItemClick: (Message) -> Unit, modifier: Modifier = Modifier, + playButton: @Composable () -> Unit, ) { val painter = rememberStreamImagePainter(attachment.imagePreviewUrl) @@ -266,26 +316,38 @@ internal fun MediaAttachmentContentItem( ) if (attachment.type == AttachmentType.VIDEO) { - // TODO add attributes such as play button shape and size to ChatTheme - Box( + playButton() + } + } +} + +/** + * A simple play button that is overlaid above + * video attachments. + */ +@Composable +internal fun PlayButton() { + Box( + modifier = Modifier + .shadow(10.dp, shape = CircleShape) + .background(color = Color.White, shape = CircleShape) + .size( + width = 42.dp, + height = 42.dp + ), + contentAlignment = Alignment.Center + ) { + Column { + Image( modifier = Modifier - .shadow(10.dp, shape = CircleShape) - .background(color = Color.White, shape = CircleShape) - .width(ChatTheme.dimens.attachmentsContentImageWidth / 6) - .aspectRatio(1f), - contentAlignment = Alignment.Center - ) { - Column { - Image( - modifier = Modifier - .alignBy { measured -> - -(measured.measuredWidth * 1 / 9) - }, - painter = painterResource(id = R.drawable.stream_compose_ic_play), - contentDescription = null, - ) - } - } + .alignBy { measured -> + // emulated offset as seen in the design specs, + // otherwise the button is visibly off to the start of the screen + -(measured.measuredWidth * 1 / 9) + }, + painter = painterResource(id = R.drawable.stream_compose_ic_play), + contentDescription = null, + ) } } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt index c0dc5234050..ced5e1e519b 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt @@ -22,22 +22,32 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.runtime.Composable import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentContent +import io.getstream.chat.android.compose.ui.attachments.content.PlayButton import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.uiutils.constant.AttachmentType +/** + * An [AttachmentFactory] that is able to handle Image and Video attachments. + * + * @param playButton Displays a play button above video attachments. + */ @Suppress("FunctionName") -public fun MediaAttachmentFactory(): AttachmentFactory = AttachmentFactory( - canHandle = { - it.none { attachment -> attachment.type != AttachmentType.IMAGE && attachment.type != AttachmentType.VIDEO } - }, - previewContent = { modifier, attachments, onAttachmentRemoved -> }, - content = @Composable { modifier, state -> - MediaAttachmentContent( - modifier = modifier - .width(ChatTheme.dimens.attachmentsContentImageWidth) - .wrapContentHeight() - .heightIn(max = ChatTheme.dimens.attachmentsContentImageMaxHeight), - attachmentState = state - ) - } -) +public fun MediaAttachmentFactory( + playButton: @Composable () -> Unit = { PlayButton() }, +): AttachmentFactory = + AttachmentFactory( + canHandle = { + it.none { attachment -> attachment.type != AttachmentType.IMAGE && attachment.type != AttachmentType.VIDEO } + }, + previewContent = { modifier, attachments, onAttachmentRemoved -> }, + content = @Composable { modifier, state -> + MediaAttachmentContent( + modifier = modifier + .width(ChatTheme.dimens.attachmentsContentImageWidth) + .wrapContentHeight() + .heightIn(max = ChatTheme.dimens.attachmentsContentImageMaxHeight), + attachmentState = state, + playButton = { playButton() } + ) + } + ) From 741387d2995c70aef11e8ed95b472725073e7209 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 22 Aug 2022 17:02:04 +0200 Subject: [PATCH 08/69] [3369] Appease detekt --- .../ui/attachments/content/MediaAttachmentContent.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 11ab3498e4d..32f3f4a98ca 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -93,7 +93,9 @@ public fun MediaAttachmentContent( horizontalArrangement = Arrangement.spacedBy(gridSpacing) ) { val attachments = - message.attachments.filter { !it.hasLink() && it.type == AttachmentType.IMAGE || it.type == AttachmentType.VIDEO } + message.attachments.filter { + !it.hasLink() && it.type == AttachmentType.IMAGE || it.type == AttachmentType.VIDEO + } val attachmentCount = attachments.size if (attachmentCount == 1) { @@ -178,6 +180,7 @@ internal fun ShowSingleMediaAttachment( * @param playButton Represents the play button that is overlaid above video attachment * previews. */ +@Suppress("LongParameterList") @Composable internal fun RowScope.ShowMultipleMediaAttachments( attachments: List, @@ -271,6 +274,7 @@ internal fun RowScope.ShowMultipleMediaAttachments( * @param playButton Represents the play button that is overlaid above video attachment * previews. */ +@Suppress("LongParameterList") @OptIn(ExperimentalFoundationApi::class) @Composable internal fun MediaAttachmentContentItem( From 51f429d6bdaf04aeea9047f4bb243d949549f3be Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 22 Aug 2022 17:03:07 +0200 Subject: [PATCH 09/69] [3369] Appease detekt --- .../compose/ui/attachments/content/MediaAttachmentContent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 32f3f4a98ca..343c894c254 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -180,7 +180,7 @@ internal fun ShowSingleMediaAttachment( * @param playButton Represents the play button that is overlaid above video attachment * previews. */ -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LongMethod") @Composable internal fun RowScope.ShowMultipleMediaAttachments( attachments: List, From 8ce405a6c581dae91516b7f54a1b91f65badaae5 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 22 Aug 2022 17:09:20 +0200 Subject: [PATCH 10/69] [3369] Appease detekt --- stream-chat-android-compose/detekt-baseline.xml | 3 +++ stream-chat-android-ui-components-sample/detekt-baseline.xml | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index 997efe13c45..fe1de1efa76 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -28,6 +28,9 @@ MagicNumber:ImageAttachmentContent.kt$3 MagicNumber:ImageAttachmentContent.kt$4 MagicNumber:ImagePreviewActivity.kt$ImagePreviewActivity$8f + MagicNumber:MediaAttachmentContent.kt$3 + MagicNumber:MediaAttachmentContent.kt$4 + MagicNumber:MediaAttachmentContent.kt$9 MagicNumber:Messages.kt$3 MagicNumber:Messages.kt$5 MagicNumber:SearchInput.kt$8f diff --git a/stream-chat-android-ui-components-sample/detekt-baseline.xml b/stream-chat-android-ui-components-sample/detekt-baseline.xml index d24f645083c..cb06cb40376 100644 --- a/stream-chat-android-ui-components-sample/detekt-baseline.xml +++ b/stream-chat-android-ui-components-sample/detekt-baseline.xml @@ -5,7 +5,6 @@ ComplexCondition:UserRepository.kt$UserRepository$apiKey != null && id != null && name != null && token != null && image != null ComplexMethod:ChatFragment.kt$ChatFragment$private fun initMessagesViewModel() ForbiddenComment:ChatInfoViewModel.kt$ChatInfoViewModel$// TODO: Handle error - ForbiddenComment:HomeFragmentViewModel.kt$HomeFragmentViewModel.State$// TODO: implement unread mentions count LongMethod:ChatFragment.kt$ChatFragment$private fun initMessagesViewModel() LongMethod:ComponentBrowserAvatarViewFragment.kt$ComponentBrowserAvatarViewFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) LongMethod:ComponentBrowserViewReactionsFragment.kt$ComponentBrowserViewReactionsFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) From db11206472d63fb6cf67a6d9366d489455effea6 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 22 Aug 2022 18:56:44 +0200 Subject: [PATCH 11/69] [3369] Flesh out the preview content for media attachments. --- .../api/stream-chat-android-compose.api | 12 +- .../content/MediaAttachmentContent.kt | 21 ++-- .../content/MediaAttachmentPreviewContent.kt | 116 ++++++++++++++++++ .../factory/MediaAttachmentFactory.kt | 68 +++++++++- 4 files changed, 194 insertions(+), 23 deletions(-) create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 9f4fd3b29ce..8c7f896da38 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -568,6 +568,10 @@ public final class io/getstream/chat/android/compose/ui/attachments/content/Medi public static final fun MediaAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V } +public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContentKt { + public static final fun MediaAttachmentPreviewContent (Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + public final class io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContentKt { public static final fun MessageAttachmentsContent (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } @@ -604,10 +608,10 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Comp public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; - public static field lambda-2 Lkotlin/jvm/functions/Function5; + public static field lambda-2 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function5; + public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$QuotedAttachmentFactoryKt { @@ -641,8 +645,8 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Link } public final class io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactoryKt { - public static final fun MediaAttachmentFactory (Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; - public static synthetic fun MediaAttachmentFactory$default (Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; + public static final fun MediaAttachmentFactory (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; + public static synthetic fun MediaAttachmentFactory$default (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; } public final class io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactoryKt { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 343c894c254..015ccf5d188 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -30,9 +30,7 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Text import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable @@ -42,14 +40,11 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import com.getstream.sdk.chat.utils.extensions.imagePreviewUrl import io.getstream.chat.android.client.models.Attachment import io.getstream.chat.android.client.models.Message @@ -328,17 +323,15 @@ internal fun MediaAttachmentContentItem( /** * A simple play button that is overlaid above * video attachments. + * + * @param modifier The modifier used for styling. */ @Composable -internal fun PlayButton() { +internal fun PlayButton( + modifier: Modifier = Modifier +) { Box( - modifier = Modifier - .shadow(10.dp, shape = CircleShape) - .background(color = Color.White, shape = CircleShape) - .size( - width = 42.dp, - height = 42.dp - ), + modifier = modifier, contentAlignment = Alignment.Center ) { Column { @@ -347,7 +340,7 @@ internal fun PlayButton() { .alignBy { measured -> // emulated offset as seen in the design specs, // otherwise the button is visibly off to the start of the screen - -(measured.measuredWidth * 1 / 9) + -(measured.measuredWidth * 1 / 8) }, painter = painterResource(id = R.drawable.stream_compose_ic_play), contentDescription = null, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt new file mode 100644 index 00000000000..c62dba34f09 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.attachments.content + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.getstream.sdk.chat.utils.extensions.imagePreviewUrl +import io.getstream.chat.android.client.models.Attachment +import io.getstream.chat.android.compose.ui.components.CancelIcon +import io.getstream.chat.android.compose.ui.components.composer.MessageInput +import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter +import io.getstream.chat.android.uiutils.constant.AttachmentType + +/** + * UI for currently selected image and video attachments, within the [MessageInput]. + * + * @param attachments Selected attachments. + * @param onAttachmentRemoved Handler when the user removes an attachment from the list. + * @param modifier Modifier for styling. + */ +@Composable +public fun MediaAttachmentPreviewContent( + attachments: List, + onAttachmentRemoved: (Attachment) -> Unit, + playButton: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + LazyRow( + modifier = modifier.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start) + ) { + items(attachments) { image -> + MediaAttachmentPreviewItem( + mediaAttachment = image, + onAttachmentRemoved = onAttachmentRemoved, + playButton = playButton + ) + } + } +} + +/** + * A preview of an individual selected image or video attachment. + * + * @param mediaAttachment The selected attachment. + * @param onAttachmentRemoved Handler when the user removes an attachment from the list. + * @param playButton Represents the play button that is overlaid above video attachment + * previews. + */ +@Composable +private fun MediaAttachmentPreviewItem( + mediaAttachment: Attachment, + onAttachmentRemoved: (Attachment) -> Unit, + playButton: @Composable () -> Unit, +) { + val painter = rememberStreamImagePainter(data = mediaAttachment.upload ?: mediaAttachment.imagePreviewUrl) + + Box( + modifier = Modifier + .size(MediaAttachmentPreviewItemSize.dp) + .clip(RoundedCornerShape(16.dp)), + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier.fillMaxSize(), + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop + ) + + if (mediaAttachment.type == AttachmentType.VIDEO) { + playButton() + } + + CancelIcon( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(4.dp), + onClick = { onAttachmentRemoved(mediaAttachment) } + ) + } +} + +/** + * The default size of the [MediaAttachmentPreviewItem] + * composable. + */ +internal const val MediaAttachmentPreviewItemSize: Int = 95 diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt index ced5e1e519b..f2fdc43eb70 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt @@ -16,12 +16,21 @@ package io.getstream.chat.android.compose.ui.attachments.factory +import androidx.compose.foundation.background import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentContent +import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentPreviewContent +import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentPreviewItemSize import io.getstream.chat.android.compose.ui.attachments.content.PlayButton import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.uiutils.constant.AttachmentType @@ -29,17 +38,30 @@ import io.getstream.chat.android.uiutils.constant.AttachmentType /** * An [AttachmentFactory] that is able to handle Image and Video attachments. * - * @param playButton Displays a play button above video attachments. + * @param contentPlayButton Displays a play button above video attachments + * in the messages list. + * @param previewContentPlayButton Displays a play button above video attachments + * in the message input. */ @Suppress("FunctionName") public fun MediaAttachmentFactory( - playButton: @Composable () -> Unit = { PlayButton() }, + contentPlayButton: @Composable () -> Unit = { DefaultContentPlayButton() }, + previewContentPlayButton: @Composable () -> Unit = { DefaultPreviewContentPlayButton() }, ): AttachmentFactory = AttachmentFactory( canHandle = { - it.none { attachment -> attachment.type != AttachmentType.IMAGE && attachment.type != AttachmentType.VIDEO } + it.none { attachment -> + attachment.type != AttachmentType.IMAGE && attachment.type != AttachmentType.VIDEO + } + }, + previewContent = { modifier, attachments, onAttachmentRemoved -> + MediaAttachmentPreviewContent( + attachments = attachments, + onAttachmentRemoved = onAttachmentRemoved, + modifier = modifier, + playButton = previewContentPlayButton + ) }, - previewContent = { modifier, attachments, onAttachmentRemoved -> }, content = @Composable { modifier, state -> MediaAttachmentContent( modifier = modifier @@ -47,7 +69,43 @@ public fun MediaAttachmentFactory( .wrapContentHeight() .heightIn(max = ChatTheme.dimens.attachmentsContentImageMaxHeight), attachmentState = state, - playButton = { playButton() } + playButton = { contentPlayButton() } ) } ) + +/** + * Represents the default play button that is + * overlaid above video attachment previews inside + * the messages list. + */ +@Composable +private fun DefaultContentPlayButton() { + PlayButton( + modifier = Modifier + .shadow(10.dp, shape = CircleShape) + .background(color = Color.White, shape = CircleShape) + .size( + width = 42.dp, + height = 42.dp + ) + ) +} + +/** + * Represents the default play button that is + * overlaid above video attachment previews inside + * the message input. + */ +@Composable +private fun DefaultPreviewContentPlayButton() { + PlayButton( + modifier = Modifier + .shadow(10.dp, shape = CircleShape) + .background(color = Color.White, shape = CircleShape) + .size( + width = (MediaAttachmentPreviewItemSize / 4).dp, + height = (MediaAttachmentPreviewItemSize / 4).dp + ) + ) +} From 22b4ddb7b6914edb5a9b048efa10a6e582b494bd Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 22 Aug 2022 18:59:35 +0200 Subject: [PATCH 12/69] [3369] Generate detekt baseline. --- stream-chat-android-compose/detekt-baseline.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index fe1de1efa76..81810c8bc8c 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -30,7 +30,8 @@ MagicNumber:ImagePreviewActivity.kt$ImagePreviewActivity$8f MagicNumber:MediaAttachmentContent.kt$3 MagicNumber:MediaAttachmentContent.kt$4 - MagicNumber:MediaAttachmentContent.kt$9 + MagicNumber:MediaAttachmentContent.kt$8 + MagicNumber:MediaAttachmentFactory.kt$4 MagicNumber:Messages.kt$3 MagicNumber:Messages.kt$5 MagicNumber:SearchInput.kt$8f From fe644ecea0d94a24c2d78d0db81e2d089b399e8d Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Wed, 24 Aug 2022 12:55:03 +0200 Subject: [PATCH 13/69] [3369] Create a detailed outline of the media gallery screen feature --- .../src/main/AndroidManifest.xml | 4 + .../MediaGalleryPreviewAction.kt | 48 + .../MediaGalleryPreviewOption.kt | 37 + .../MediaGalleryPreviewResult.kt | 47 + .../messages/attachments/AttachmentState.kt | 5 + .../content/MediaAttachmentContent.kt | 89 +- .../preview/MediaGalleryPreviewActivity.kt | 1209 +++++++++++++++++ .../preview/MediaGalleryPreviewContract.kt | 62 + .../MediaGalleryPreviewViewModel.kt | 119 ++ .../MediaGalleryPreviewViewModelFactory.kt | 39 + .../src/main/res/values/strings.xml | 10 + 11 files changed, 1626 insertions(+), 43 deletions(-) create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewAction.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewOption.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModelFactory.kt diff --git a/stream-chat-android-compose/src/main/AndroidManifest.xml b/stream-chat-android-compose/src/main/AndroidManifest.xml index 6fd94dfc48c..46b06a50e56 100644 --- a/stream-chat-android-compose/src/main/AndroidManifest.xml +++ b/stream-chat-android-compose/src/main/AndroidManifest.xml @@ -31,5 +31,9 @@ android:name=".ui.attachments.preview.MediaPreviewActivity" android:exported="false" /> + diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewAction.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewAction.kt new file mode 100644 index 00000000000..49f2c16a393 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewAction.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.state.mediagallerypreview + +import io.getstream.chat.android.client.models.Message + +/** + * Represents the actions the user can take with media attachments in the Media Gallery Preview + * feature. + * + * @param message The message that the action is being performed on. + */ +internal sealed class MediaGalleryPreviewAction(internal val message: Message) + +/** + * Should take the user back to the message list with a pre-packaged + * quoted reply in the message input. + */ +internal class Reply(message: Message) : MediaGalleryPreviewAction(message) + +/** + * Should show the message containing the attachments in the message list. + */ +internal class ShowInChat(message: Message) : MediaGalleryPreviewAction(message) + +/** + * Should save the media to storage. + */ +internal class SaveMedia(message: Message) : MediaGalleryPreviewAction(message) + +/** + * Should remove the selected media attachment from the original message. + */ +internal class Delete(message: Message) : MediaGalleryPreviewAction(message) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewOption.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewOption.kt new file mode 100644 index 00000000000..55d7e43b32e --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewOption.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.state.mediagallerypreview + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter + +/** + * Represents the information for media gallery preview options the user can take. + * + * @param title The title of the option in the list. + * @param titleColor The color of the title option. + * @param iconPainter The icon of the option. + * @param iconColor The color of the icon. + * @param action The action this option represents. + */ +internal data class MediaGalleryPreviewOption( + internal val title: String, + internal val titleColor: Color, + internal val iconPainter: Painter, + internal val iconColor: Color, + internal val action: MediaGalleryPreviewAction, +) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult.kt new file mode 100644 index 00000000000..c743377eff7 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.state.mediagallerypreview + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Represents the Media Gallery Preview screen result that we propagate to the Messages screen. + * + * @param messageId The ID of the message that we've selected. + * @param resultType The action that will be executed on the message list screen. + */ +@Parcelize +public class MediaGalleryPreviewResult( + public val messageId: String, + public val resultType: MediaGalleryPreviewResultType, +) : Parcelable + +/** + * Represents the types of actions that result in different behavior in the message list. + */ +public enum class MediaGalleryPreviewResultType { + /** + * The action when the user wants to scroll to and focus a given image. + */ + SHOW_IN_CHAT, + + /** + * The action when the user wants to quote and reply to a message. + */ + QUOTE, +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt index 040b8ceae52..62b0ed9c0d7 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt @@ -18,6 +18,7 @@ package io.getstream.chat.android.compose.state.messages.attachments import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResult +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult /** * Represents the state of Attachment items, used to render and add handlers required for the attachment to work. @@ -25,9 +26,13 @@ import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResult * @param message Data that represents the message information. * @param onLongItemClick Handler for a long click on the message item. * @param onImagePreviewResult Handler when the user selects an action to scroll to and focus an image. + * @param onMediaPreviewResult Handler used when the user selects an action to perform from + * [io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewActivity]. */ public data class AttachmentState( val message: Message, val onLongItemClick: (Message) -> Unit = {}, val onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, + // TODO structure better with a deprecation process + val onMediaPreviewResult: (MediaGalleryPreviewResult?) -> Unit ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 015ccf5d188..25ce4fd2693 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -49,9 +49,9 @@ import com.getstream.sdk.chat.utils.extensions.imagePreviewUrl import io.getstream.chat.android.client.models.Attachment import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResult +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult import io.getstream.chat.android.compose.state.messages.attachments.AttachmentState -import io.getstream.chat.android.compose.ui.attachments.preview.ImagePreviewContract +import io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewContract import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter import io.getstream.chat.android.uiutils.constant.AttachmentType @@ -73,7 +73,8 @@ public fun MediaAttachmentContent( modifier: Modifier = Modifier, playButton: @Composable () -> Unit = { PlayButton() }, ) { - val (message, onLongItemClick, onImagePreviewResult) = attachmentState + val (message, onLongItemClick, _, onMediaGalleryPreviewResult) = attachmentState + // TODO add media grid spacing to chat theme val gridSpacing = ChatTheme.dimens.attachmentsContentImageGridSpacing Row( @@ -99,7 +100,7 @@ public fun MediaAttachmentContent( ShowSingleMediaAttachment( attachment = attachment, message = message, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, playButton = playButton ) @@ -109,7 +110,7 @@ public fun MediaAttachmentContent( attachmentCount = attachmentCount, gridSpacing = gridSpacing, message = message, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, playButton = playButton ) @@ -122,8 +123,8 @@ public fun MediaAttachmentContent( * * @param attachment The attachment that is previewed. * @param message The original message containing the attachment. - * @param onImagePreviewResult The result of the activity used for propagating - * actions such as image selection, deletion, etc. + * @param onMediaGalleryPreviewResult The result of the activity used for propagating + * actions such as media attachment selection, deletion, etc. * @param onLongItemClick Lambda that gets called when an item is long clicked. * @param playButton Represents the play button that is overlaid above video attachment * previews. @@ -132,7 +133,7 @@ public fun MediaAttachmentContent( internal fun ShowSingleMediaAttachment( attachment: Attachment, message: Message, - onImagePreviewResult: (ImagePreviewResult?) -> Unit, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, onLongItemClick: (Message) -> Unit, playButton: @Composable () -> Unit, ) { @@ -156,7 +157,7 @@ internal fun ShowSingleMediaAttachment( .aspectRatio(ratio ?: EqualDimensionsRatio), message = message, attachmentPosition = 0, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, playButton = playButton ) @@ -169,8 +170,8 @@ internal fun ShowSingleMediaAttachment( * @param attachmentCount The number of attachments that are to be previewed. * @param gridSpacing Determines the spacing strategy between items. * @param message The original message containing the attachments. - * @param onImagePreviewResult The result of the activity used for propagating - * actions such as image selection, deletion, etc. + * @param onMediaGalleryPreviewResult The result of the activity used for propagating + * actions such as media attachment selection, deletion, etc. * @param onLongItemClick Lambda that gets called when an item is long clicked. * @param playButton Represents the play button that is overlaid above video attachment * previews. @@ -182,7 +183,7 @@ internal fun RowScope.ShowMultipleMediaAttachments( attachmentCount: Int, gridSpacing: Dp, message: Message, - onImagePreviewResult: (ImagePreviewResult?) -> Unit, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, onLongItemClick: (Message) -> Unit, playButton: @Composable () -> Unit, ) { @@ -193,14 +194,14 @@ internal fun RowScope.ShowMultipleMediaAttachments( .aspectRatio(TwiceAsTallAsIsWideRatio), verticalArrangement = Arrangement.spacedBy(gridSpacing) ) { - for (imageIndex in 0..3 step 2) { - if (imageIndex < attachmentCount) { + for (attachmentIndex in 0..3 step 2) { + if (attachmentIndex < attachmentCount) { MediaAttachmentContentItem( - attachment = attachments[imageIndex], + attachment = attachments[attachmentIndex], modifier = Modifier.weight(1f), message = message, - attachmentPosition = imageIndex, - onImagePreviewResult = onImagePreviewResult, + attachmentPosition = attachmentIndex, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, playButton = playButton ) @@ -214,26 +215,26 @@ internal fun RowScope.ShowMultipleMediaAttachments( .aspectRatio(TwiceAsTallAsIsWideRatio), verticalArrangement = Arrangement.spacedBy(gridSpacing) ) { - for (imageIndex in 1..4 step 2) { - if (imageIndex < attachmentCount) { - val attachment = attachments[imageIndex] + for (attachmentIndex in 1..4 step 2) { + if (attachmentIndex < attachmentCount) { + val attachment = attachments[attachmentIndex] val isUploading = attachment.uploadState is Attachment.UploadState.InProgress - if (imageIndex == 3 && attachmentCount > 4) { + if (attachmentIndex == 3 && attachmentCount > 4) { Box(modifier = Modifier.weight(1f)) { MediaAttachmentContentItem( attachment = attachment, message = message, - attachmentPosition = imageIndex, - onImagePreviewResult = onImagePreviewResult, + attachmentPosition = attachmentIndex, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, playButton = playButton ) if (!isUploading) { MediaAttachmentViewMoreOverlay( - imageCount = attachmentCount, - imageIndex = imageIndex, + mediaCount = attachmentCount, + mediaIndex = attachmentIndex, modifier = Modifier.align(Alignment.Center) ) } @@ -243,8 +244,8 @@ internal fun RowScope.ShowMultipleMediaAttachments( attachment = attachment, modifier = Modifier.weight(1f), message = message, - attachmentPosition = imageIndex, - onImagePreviewResult = onImagePreviewResult, + attachmentPosition = attachmentIndex, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, playButton = playButton ) @@ -262,8 +263,8 @@ internal fun RowScope.ShowMultipleMediaAttachments( * of attachments. Used to remember the item position when viewing it in a separate * activity. * @param attachment The attachment that is previewed. - * @param onImagePreviewResult The result of the activity used for propagating - * actions such as image selection, deletion, etc. + * @param onMediaGalleryPreviewResult The result of the activity used for propagating + * actions such as media attachment selection, deletion, etc. * @param onLongItemClick Lambda that gets called when the item is long clicked. * @param modifier Modifier used for styling. * @param playButton Represents the play button that is overlaid above video attachment @@ -276,16 +277,16 @@ internal fun MediaAttachmentContentItem( message: Message, attachmentPosition: Int, attachment: Attachment, - onImagePreviewResult: (ImagePreviewResult?) -> Unit, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit, onLongItemClick: (Message) -> Unit, modifier: Modifier = Modifier, playButton: @Composable () -> Unit, ) { val painter = rememberStreamImagePainter(attachment.imagePreviewUrl) - val imagePreviewLauncher = rememberLauncherForActivityResult( - contract = ImagePreviewContract(), - onResult = { result -> onImagePreviewResult(result) } + val mixedMediaPreviewLauncher = rememberLauncherForActivityResult( + contract = MediaGalleryPreviewContract(), + onResult = { result -> onMediaGalleryPreviewResult(result) } ) Box( @@ -295,8 +296,8 @@ internal fun MediaAttachmentContentItem( interactionSource = MutableInteractionSource(), indication = rememberRipple(), onClick = { - imagePreviewLauncher.launch( - ImagePreviewContract.Input( + mixedMediaPreviewLauncher.launch( + MediaGalleryPreviewContract.Input( messageId = message.id, initialPosition = attachmentPosition ) @@ -337,6 +338,7 @@ internal fun PlayButton( Column { Image( modifier = Modifier + .fillMaxSize(0.8f) .alignBy { measured -> // emulated offset as seen in the design specs, // otherwise the button is visibly off to the start of the screen @@ -350,19 +352,20 @@ internal fun PlayButton( } /** - * Represents an overlay that's shown on the last image in the image attachment item gallery. + * Represents an overlay that's shown on the last media attachment preview in the media attachment + * item gallery. * - * @param imageCount The number of total images. - * @param imageIndex The current image index. + * @param mediaCount The number of total media attachments. + * @param mediaIndex The current media attachment index. * @param modifier Modifier for styling. */ @Composable internal fun MediaAttachmentViewMoreOverlay( - imageCount: Int, - imageIndex: Int, + mediaCount: Int, + mediaIndex: Int, modifier: Modifier = Modifier, ) { - val remainingImagesCount = imageCount - (imageIndex + 1) + val remainingMediaCount = mediaCount - (mediaIndex + 1) Box( modifier = Modifier @@ -373,8 +376,8 @@ internal fun MediaAttachmentViewMoreOverlay( modifier = modifier .wrapContentSize(), text = stringResource( - id = R.string.stream_compose_remaining_images_count, - remainingImagesCount + id = R.string.stream_compose_remaining_media_attachments_count, + remainingMediaCount ), color = ChatTheme.colors.barsBackground, style = ChatTheme.typography.title1, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt new file mode 100644 index 00000000000..c4cfd8f5606 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -0,0 +1,1209 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.attachments.preview + +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import android.Manifest +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.MediaController +import android.widget.VideoView +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.calculatePan +import androidx.compose.foundation.gestures.calculateZoom +import androidx.compose.foundation.gestures.forEachGesture +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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 +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import coil.compose.AsyncImagePainter +import com.getstream.sdk.chat.StreamFileUtil +import com.getstream.sdk.chat.images.StreamImageLoader +import com.getstream.sdk.chat.utils.extensions.imagePreviewUrl +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.PagerState +import com.google.accompanist.pager.rememberPagerState +import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.client.models.Attachment +import io.getstream.chat.android.client.models.Message +import io.getstream.chat.android.client.models.User +import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.handlers.DownloadPermissionHandler +import io.getstream.chat.android.compose.handlers.PermissionHandler +import io.getstream.chat.android.compose.state.mediagallerypreview.Delete +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewAction +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewOption +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResultType +import io.getstream.chat.android.compose.state.mediagallerypreview.Reply +import io.getstream.chat.android.compose.state.mediagallerypreview.SaveMedia +import io.getstream.chat.android.compose.state.mediagallerypreview.ShowInChat +import io.getstream.chat.android.compose.ui.attachments.content.PlayButton +import io.getstream.chat.android.compose.ui.components.LoadingIndicator +import io.getstream.chat.android.compose.ui.components.Timestamp +import io.getstream.chat.android.compose.ui.components.avatar.UserAvatar +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter +import io.getstream.chat.android.compose.viewmodel.mediapreview.MediaGalleryPreviewViewModel +import io.getstream.chat.android.compose.viewmodel.mediapreview.MediaGalleryPreviewViewModelFactory +import io.getstream.chat.android.uiutils.constant.AttachmentType +import kotlinx.coroutines.launch +import java.util.Date +import kotlin.math.abs + +/** + * Shows an image and video previews along with enabling + * the user to perform various actions such as image or file deletion. + */ +@OptIn(ExperimentalPagerApi::class) +public class MediaGalleryPreviewActivity : AppCompatActivity() { + + /** + * Factory used to build the screen ViewModel given the received message ID. + */ + private val factory by lazy { + MediaGalleryPreviewViewModelFactory( + chatClient = ChatClient.instance(), + messageId = intent?.getStringExtra(KeyMessageId) ?: "" + ) + } + + /** + * The ViewModel that exposes screen data. + */ + private val mediaGalleryPreviewViewModel by viewModels(factoryProducer = { factory }) + + /** + * Sets up the data required to show the previews of images or videos within the given message. + * + * Immediately finishes in case the data is invalid. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val messageId = intent?.getStringExtra(KeyMessageId) ?: "" + val attachmentPosition = intent?.getIntExtra(KeyAttachmentPosition, 0) ?: 0 + + if (messageId.isBlank()) { + throw IllegalArgumentException("Missing messageId necessary to load images.") + } + + setContent { + ChatTheme { + val message = mediaGalleryPreviewViewModel.message + + if (message.deletedAt != null) { + finish() + return@ChatTheme + } + + if (message.id.isNotEmpty()) { + MediaGalleryPreviewContentWrapper(message, attachmentPosition) + } + } + } + } + + /** + * Wraps the content of the screen in a composable that represents the top and bottom bars and the + * image and video previews. + * + * @param message The message to show the attachments from. + * @param initialAttachmentPosition The initial pager position, based on the attachment preview + * the user clicked on. + */ + @OptIn(ExperimentalAnimationApi::class) + @Composable + private fun MediaGalleryPreviewContentWrapper( + message: Message, + initialAttachmentPosition: Int, + ) { + val startingPosition = + if (initialAttachmentPosition !in message.attachments.indices) 0 else initialAttachmentPosition + + val pagerState = rememberPagerState(initialPage = startingPosition) + + Box(modifier = Modifier.fillMaxSize()) { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { MediaGalleryPreviewTopBar(message) }, + content = { contentPadding -> + Surface( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + ) { + MediaPreviewContent(pagerState, message.attachments) + } + }, + bottomBar = { MediaGalleryPreviewBottomBar(message.attachments, pagerState) } + ) + + AnimatedVisibility( + visible = mediaGalleryPreviewViewModel.isShowingOptions, + enter = fadeIn(), + exit = fadeOut() + ) { + MediaGalleryPreviewOptions( + options = defaultMediaOptions(message = message), + pagerState = pagerState, + modifier = Modifier.animateEnterExit( + enter = slideInVertically(), + exit = slideOutVertically() + ) + ) + } + + AnimatedVisibility( + visible = mediaGalleryPreviewViewModel.isShowingGallery, + enter = fadeIn(), + exit = fadeOut() + ) { + MediaGallery( + pagerState = pagerState, + modifier = Modifier.animateEnterExit( + enter = slideInVertically(initialOffsetY = { height -> height / 2 }), + exit = slideOutVertically(targetOffsetY = { height -> height / 2 }) + ) + ) + } + } + } + + /** + * The top bar which allows the user to go back or browse more screen options. + * + * @param message The message used for info and actions. + */ + @Composable + private fun MediaGalleryPreviewTopBar(message: Message) { + Surface( + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + elevation = 4.dp, + color = ChatTheme.colors.barsBackground + ) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton(onClick = ::finish) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_ic_close), + contentDescription = stringResource(id = R.string.stream_compose_cancel), + tint = ChatTheme.colors.textHighEmphasis, + ) + } + + MediaGalleryPreviewHeaderTitle( + modifier = Modifier.weight(8f), + message = message + ) + + MediaGalleryPreviewOptionsToggle(modifier = Modifier.weight(1f)) + } + } + } + + /** + * Represents the header title that shows more information about the media attachments. + * + * @param message The message with the media attachments we're observing. + * @param modifier Modifier for styling. + */ + @Composable + private fun MediaGalleryPreviewHeaderTitle( + message: Message, + modifier: Modifier = Modifier, + ) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = message.user.name, + style = ChatTheme.typography.title3Bold, + color = ChatTheme.colors.textHighEmphasis + ) + + Timestamp(date = message.updatedAt ?: message.createdAt ?: Date()) + } + } + + /** + * Toggles the media attachments options menu. + * + * @param modifier Modifier for styling. + */ + @Composable + private fun MediaGalleryPreviewOptionsToggle( + modifier: Modifier = Modifier, + ) { + Icon( + modifier = modifier + .size(24.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onClick = { mediaGalleryPreviewViewModel.toggleMediaOptions(isShowingOptions = true) } + ), + painter = painterResource(id = R.drawable.stream_compose_ic_menu_vertical), + contentDescription = stringResource(R.string.stream_compose_image_options), + tint = ChatTheme.colors.textHighEmphasis + ) + } + + /** + * The media attachment options menu, used to perform different actions for the currently active media + * attachment. + * + * @param options The options available for the attachment. + * @param pagerState The state of the pager, used to fetch the current attachment. + * @param modifier Modifier for styling. + */ + @Composable + private fun MediaGalleryPreviewOptions( + options: List, + pagerState: PagerState, + modifier: Modifier, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.overlay) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { mediaGalleryPreviewViewModel.toggleMediaOptions(isShowingOptions = false) } + ) + ) { + Surface( + modifier = modifier + .padding(16.dp) + .width(150.dp) + .wrapContentHeight() + .align(Alignment.TopEnd), + shape = RoundedCornerShape(16.dp), + elevation = 4.dp, + color = ChatTheme.colors.barsBackground + ) { + Column(modifier = Modifier.fillMaxWidth()) { + options.forEachIndexed { index, option -> + MediaGalleryPreviewOptionItem(option, pagerState) + + if (index != options.lastIndex) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(0.5.dp) + .background(ChatTheme.colors.borders) + ) + } + } + } + } + } + } + + /** + * Represents each item in the media options menu that the user can pick. + * + * @param mediaGalleryPreviewOption The option information to show. + * @param pagerState The state of the pager, used to handle selected actions. + */ + @Composable + private fun MediaGalleryPreviewOptionItem( + mediaGalleryPreviewOption: MediaGalleryPreviewOption, + pagerState: PagerState, + ) { + val downloadPermissionHandler = ChatTheme.permissionHandlerProvider + .first { it.canHandle(Manifest.permission.WRITE_EXTERNAL_STORAGE) } + + Row( + modifier = Modifier + .fillMaxWidth() + .background(ChatTheme.colors.barsBackground) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = { + mediaGalleryPreviewViewModel.toggleMediaOptions(isShowingOptions = false) + handleMediaAction( + mediaGalleryPreviewOption.action, + pagerState.currentPage, + downloadPermissionHandler + ) + } + ) + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(8.dp)) + + Icon( + modifier = Modifier + .size(18.dp), + painter = mediaGalleryPreviewOption.iconPainter, + tint = mediaGalleryPreviewOption.iconColor, + contentDescription = mediaGalleryPreviewOption.title + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = mediaGalleryPreviewOption.title, + color = mediaGalleryPreviewOption.titleColor, + style = ChatTheme.typography.bodyBold, + fontSize = 12.sp + ) + } + } + + /** + * Consumes the action user selected to perform for the current media attachment. + * + * @param mediaGalleryPreviewAction The action the user selected. + * @param currentPage The index of the current media attachment. + * @param permissionHandler Checks if we have the necessary permissions + * to perform an action if the action needs a specific Android permission. + */ + private fun handleMediaAction( + mediaGalleryPreviewAction: MediaGalleryPreviewAction, + currentPage: Int, + permissionHandler: PermissionHandler, + ) { + val message = mediaGalleryPreviewAction.message + + when (mediaGalleryPreviewAction) { + is ShowInChat -> { + handleResult( + MediaGalleryPreviewResult( + messageId = message.id, + resultType = MediaGalleryPreviewResultType.SHOW_IN_CHAT + ) + ) + } + is Reply -> { + handleResult(MediaGalleryPreviewResult(messageId = message.id, resultType = MediaGalleryPreviewResultType.QUOTE)) + } + is Delete -> mediaGalleryPreviewViewModel.deleteCurrentMediaAttachment(message.attachments[currentPage]) + is SaveMedia -> { + permissionHandler + .onHandleRequest( + mapOf(DownloadPermissionHandler.PayloadAttachment to message.attachments[currentPage]) + ) + } + } + } + + /** + * Prepares and sets the result of this Activity and propagates it back to the user. + * + * @param result The chosen action result. + */ + private fun handleResult(result: MediaGalleryPreviewResult) { + val data = Intent().apply { + putExtra(KeyMediaGalleryPreviewResult, result) + } + setResult(RESULT_OK, data) + finish() + } + + /** + * Renders a horizontal pager that shows images and allows the user to swipe, zoom and pan through them. + * + * @param pagerState The state of the content pager. + * @param attachments The attachments to show within the pager. + */ + @Suppress("LongMethod", "ComplexMethod") + @Composable + private fun MediaPreviewContent( + pagerState: PagerState, + attachments: List, + ) { + if (attachments.isEmpty()) { + finish() + return + } + + HorizontalPager( + modifier = Modifier.background(ChatTheme.colors.appBackground), + state = pagerState, + count = attachments.size, + ) { page -> + if (attachments[page].type == AttachmentType.IMAGE) { + ImagePreviewContent(attachment = attachments[page], pagerState = pagerState, page = page) + } else if (attachments[page].type == AttachmentType.VIDEO) { + VideoPreviewContent( + attachment = attachments[page], + pagerState = pagerState, + page = page, + onPlaybackError = { + // TODO add error + } + ) + } + } + } + + /** + * Represents an individual page containing an image that is zoomable and scrollable. + * + * @param attachment The image attachment to be displayed. + * @param pagerState The state of the pager that contains this page + * @param page The page an instance of this content is located on. + */ + @Composable + private fun ImagePreviewContent( + attachment: Attachment, + pagerState: PagerState, + page: Int, + ) { + BoxWithConstraints( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + val painter = rememberStreamImagePainter(data = attachment.imagePreviewUrl) + + val density = LocalDensity.current + val parentSize = Size(density.run { maxWidth.toPx() }, density.run { maxHeight.toPx() }) + var imageSize by remember { mutableStateOf(Size(0f, 0f)) } + + var currentScale by remember { mutableStateOf(DefaultZoomScale) } + var translation by remember { mutableStateOf(Offset(0f, 0f)) } + + val scale by animateFloatAsState(targetValue = currentScale) + + val transformModifier = if (painter.state is AsyncImagePainter.State.Success) { + val size = painter.intrinsicSize + Modifier.aspectRatio(size.width / size.height, true) + } else { + Modifier + } + + Image( + modifier = transformModifier + .graphicsLayer( + scaleY = scale, + scaleX = scale, + translationX = translation.x, + translationY = translation.y + ) + .onGloballyPositioned { + imageSize = Size(it.size.width.toFloat(), it.size.height.toFloat()) + } + .pointerInput(Unit) { + forEachGesture { + awaitPointerEventScope { + awaitFirstDown(requireUnconsumed = true) + do { + val event = awaitPointerEvent(pass = PointerEventPass.Initial) + + val zoom = event.calculateZoom() + currentScale = (zoom * currentScale).coerceAtMost(MaxZoomScale) + + val maxTranslation = calculateMaxOffset( + imageSize = imageSize, + scale = currentScale, + parentSize = parentSize + ) + + val offset = event.calculatePan() + val newTranslationX = translation.x + offset.x * currentScale + val newTranslationY = translation.y + offset.y * currentScale + + translation = Offset( + newTranslationX.coerceIn(-maxTranslation.x, maxTranslation.x), + newTranslationY.coerceIn(-maxTranslation.y, maxTranslation.y) + ) + + if (abs(newTranslationX) < calculateMaxOffsetPerAxis( + imageSize.width, + currentScale, + parentSize.width + ) || zoom != DefaultZoomScale + ) { + event.changes.forEach { it.consume() } + } + } while (event.changes.any { it.pressed }) + + if (currentScale < DefaultZoomScale) { + currentScale = DefaultZoomScale + } + } + } + } + .pointerInput(Unit) { + forEachGesture { + awaitPointerEventScope { + awaitFirstDown() + withTimeoutOrNull(DoubleTapTimeoutMs) { + awaitFirstDown() + currentScale = when { + currentScale == MaxZoomScale -> DefaultZoomScale + currentScale >= MidZoomScale -> MaxZoomScale + else -> MidZoomScale + } + + if (currentScale == DefaultZoomScale) { + translation = Offset(0f, 0f) + } + } + } + } + }, + painter = painter, + contentDescription = null + ) + + Log.d("isCurrentPage", "${page != pagerState.currentPage}") + + if (pagerState.currentPage != page) { + currentScale = DefaultZoomScale + translation = Offset(0f, 0f) + } + } + } + + /** + * Represents an individual page containing video player with media controls. + * + * @param attachment The video attachment to be played. + * @param pagerState The state of the pager that contains this page + * @param page The page an instance of this content is located on. + * @param onPlaybackError Handler for playback errors. + */ + @Composable + private fun VideoPreviewContent( + attachment: Attachment, + pagerState: PagerState, + page: Int, + onPlaybackError: () -> Unit, + ) { + val context = LocalContext.current + + var hasPrepared by remember { + mutableStateOf(false) + } + + var userHasClickedPlay by remember { + mutableStateOf(false) + } + + var shouldShowProgressBar by remember { + mutableStateOf(false) + } + + var shouldShowPreview by remember { + mutableStateOf(true) + } + + var shouldShowPlayButton by remember { + mutableStateOf(true) + } + + val mediaController = remember { + createMediaController(context) + } + + val videoView = remember { + VideoView(context) + } + + val contentView = remember { + + val frameLayout = FrameLayout(context).apply { + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + videoView.apply { + setVideoURI(Uri.parse(attachment.assetUrl)) + this.setMediaController(mediaController) + setOnErrorListener { _, _, _ -> + shouldShowProgressBar = false + onPlaybackError() + true + } + setOnPreparedListener { + // Don't remove the preview unless the user has clicked play previously, + // otherwise the preview will be removed whenever the video finished downloading. + if (!hasPrepared && userHasClickedPlay) { + shouldShowProgressBar = false + shouldShowPreview = false + mediaController.show() + } + hasPrepared = true + } + + mediaController.setAnchorView(frameLayout) + + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ).apply { + gravity = Gravity.CENTER + } + } + + frameLayout.apply { + addView(videoView) + } + } + + Box(contentAlignment = Alignment.Center) { + + AndroidView( + modifier = Modifier + .fillMaxSize() + .background(Color.Black), + factory = { contentView }, + ) + + if (shouldShowPreview) { + val painter = rememberStreamImagePainter(data = attachment.thumbUrl) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Image( + modifier = Modifier + .clickable { + shouldShowProgressBar = true + shouldShowPlayButton = false + userHasClickedPlay = true + // Don't remove the preview unless the player + // is ready to play. + if (hasPrepared) { + shouldShowProgressBar = false + shouldShowPreview = false + mediaController.show() + } + videoView.start() + } + .fillMaxSize() + .background(color = Color.Black), + painter = painter, contentDescription = null + ) + + if (shouldShowPlayButton) { + PlayButton( + modifier = Modifier + .shadow(10.dp, shape = CircleShape) + .background(color = Color.White, shape = CircleShape) + .size( + width = 42.dp, + height = 42.dp + ) + ) + } + } + } + + if (shouldShowProgressBar) { + LoadingIndicator() + } + } + + if (page != pagerState.currentPage) { + shouldShowPlayButton = true + shouldShowPreview = true + shouldShowProgressBar = false + mediaController.hide() + } + } + + /** + * Creates a custom instance of [MediaController]. + * + * @param context The Context used to create the [MediaController]. + */ + private fun createMediaController( + context: Context, + ): MediaController { + return object : MediaController(context) {} + } + + /** + * Calculates max offset that an image can have before reaching the edges. + * + * @param imageSize The size of the image that is being viewed. + * @param scale The current scale of the image that is being viewed. + * @param parentSize The size of the view containing the image being viewed. + */ + private fun calculateMaxOffset(imageSize: Size, scale: Float, parentSize: Size): Offset { + val maxTranslationY = calculateMaxOffsetPerAxis(imageSize.height, scale, parentSize.height) + val maxTranslationX = calculateMaxOffsetPerAxis(imageSize.width, scale, parentSize.width) + return Offset(maxTranslationX, maxTranslationY) + } + + /** + * Calculates max offset an image can have on a single axis. + * + * @param axisSize The size of the image on a given axis. + * @param scale The current scale of of the image. + * @param parentAxisSize The size of the parent view on a given axis. + */ + private fun calculateMaxOffsetPerAxis(axisSize: Float, scale: Float, parentAxisSize: Float): Float { + return (axisSize * scale - parentAxisSize).coerceAtLeast(0f) / 2 + } + + /** + * Represents the bottom bar which holds more options and information about the current + * media attachment. + * + * @param attachments The attachments to use for the UI state and options. + * @param pagerState The state of the pager, used for current page information. + */ + @Composable + private fun MediaGalleryPreviewBottomBar(attachments: List, pagerState: PagerState) { + val attachmentCount = attachments.size + + Surface( + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + elevation = 4.dp, + color = ChatTheme.colors.barsBackground + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) { + IconButton( + modifier = Modifier.align(Alignment.CenterStart), + onClick = { onShareMediaClick(attachments[pagerState.currentPage]) } + ) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_ic_share), + contentDescription = stringResource(id = R.string.stream_compose_image_preview_share), + tint = ChatTheme.colors.textHighEmphasis, + ) + } + + Text( + modifier = Modifier.align(Alignment.Center), + text = stringResource( + id = R.string.stream_compose_image_order, + pagerState.currentPage + 1, + attachmentCount + ), + style = ChatTheme.typography.title3Bold, + color = ChatTheme.colors.textHighEmphasis + ) + + IconButton( + modifier = Modifier.align(Alignment.CenterEnd), + onClick = { mediaGalleryPreviewViewModel.toggleGallery(isShowingGallery = true) } + ) { + Icon( + painter = painterResource(id = R.drawable.stream_compose_ic_gallery), + contentDescription = stringResource(id = R.string.stream_compose_image_preview_photos), + tint = ChatTheme.colors.textHighEmphasis, + ) + } + } + } + } + + /** + * Builds the media options based on the given [message]. These options let the user interact more + * with the media they're observing. + * + * @param message The message that holds all the media. + * @return [List] of options the user can choose from, in the form of [MediaGalleryPreviewOption]. + */ + @Composable + private fun defaultMediaOptions(message: Message): List { + val user by mediaGalleryPreviewViewModel.user.collectAsState() + val options = mutableListOf( + MediaGalleryPreviewOption( + title = stringResource(id = R.string.stream_compose_media_gallery_preview_reply), + titleColor = ChatTheme.colors.textHighEmphasis, + iconPainter = painterResource(id = R.drawable.stream_compose_ic_reply), + iconColor = ChatTheme.colors.textHighEmphasis, + action = Reply(message) + ), + MediaGalleryPreviewOption( + title = stringResource(id = R.string.stream_compose_media_gallery_preview_show_in_chat), + titleColor = ChatTheme.colors.textHighEmphasis, + iconPainter = painterResource(id = R.drawable.stream_compose_ic_show_in_chat), + iconColor = ChatTheme.colors.textHighEmphasis, + action = ShowInChat(message) + ), + MediaGalleryPreviewOption( + title = stringResource(id = R.string.stream_compose_media_gallery_preview_save_image), + titleColor = ChatTheme.colors.textHighEmphasis, + iconPainter = painterResource(id = R.drawable.stream_compose_ic_download), + iconColor = ChatTheme.colors.textHighEmphasis, + action = SaveMedia(message) + ) + ) + + if (message.user.id == user?.id) { + options.add( + MediaGalleryPreviewOption( + title = stringResource(id = R.string.stream_compose_media_gallery_preview_delete), + titleColor = ChatTheme.colors.errorAccent, + iconPainter = painterResource(id = R.drawable.stream_compose_ic_delete), + iconColor = ChatTheme.colors.errorAccent, + action = Delete(message) + ) + ) + } + + return options + } + + /** + * Handles the logic of loading the image and preparing a shareable file. + * + * @param attachment The attachment to preload and share. + */ + private fun onShareMediaClick(attachment: Attachment) { + // TODO share videos as well + lifecycleScope.launch { + val uri = StreamImageLoader.instance().loadAsBitmap( + context = applicationContext, + url = attachment.imagePreviewUrl!! + )?.let { + StreamFileUtil.writeImageToSharableFile(applicationContext, it) + } + + if (uri != null) { + shareImage(uri) + } + } + } + + /** + * Starts a picker to share the current image. + * + * @param imageUri The URI of the image to share. + */ + private fun shareImage(imageUri: Uri) { + ContextCompat.startActivity( + this, + Intent.createChooser( + Intent(Intent.ACTION_SEND).apply { + type = "image/*" + putExtra(Intent.EXTRA_STREAM, imageUri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + }, + getString(R.string.stream_compose_attachment_gallery_share), + ), + null + ) + } + + /** + * Starts a picker to share the current image. + * + * @param imageUri The URI of the image to share. + */ + private fun shareVideo(videoUri: Uri) { + // TODO + } + + /** + * Represents the image gallery where the user can browse all media attachments and quickly jump to them. + * + * @param pagerState The state of the pager, used to navigate to specific media attachments. + * @param modifier Modifier for styling. + */ + @Composable + private fun MediaGallery( + pagerState: PagerState, + modifier: Modifier = Modifier, + ) { + val message = mediaGalleryPreviewViewModel.message + + Box( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.overlay) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { mediaGalleryPreviewViewModel.toggleGallery(isShowingGallery = false) } + ) + ) { + Surface( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .align(Alignment.BottomCenter) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = {} + ), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + elevation = 4.dp, + color = ChatTheme.colors.barsBackground + ) { + Column(modifier = Modifier.fillMaxWidth()) { + + MediaGalleryHeader() + + LazyVerticalGrid( + columns = GridCells.Fixed(ColumnCount), + content = { + itemsIndexed(message.attachments) { index, attachment -> + MediaGalleryItem(index, attachment, message.user, pagerState) + } + } + ) + } + } + } + } + + /** + * Represents the header of [MediaGallery] that allows the user to dismiss the component. + */ + @Composable + private fun MediaGalleryHeader() { + Box(modifier = Modifier.fillMaxWidth()) { + Icon( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(8.dp) + .clickable( + indication = rememberRipple(), + interactionSource = remember { MutableInteractionSource() }, + onClick = { mediaGalleryPreviewViewModel.toggleGallery(isShowingGallery = false) } + ), + painter = painterResource(id = R.drawable.stream_compose_ic_close), + contentDescription = stringResource(id = R.string.stream_compose_cancel), + tint = ChatTheme.colors.textHighEmphasis + ) + + Text( + modifier = Modifier.align(Alignment.Center), + text = stringResource(R.string.stream_compose_image_preview_photos), + style = ChatTheme.typography.title3Bold, + color = ChatTheme.colors.textHighEmphasis + ) + } + } + + /** + * Represents each item in the [MediaGallery]. + * + * @param index The index of the item. + * @param attachment The attachment data used to load the item media attachment. + * @param user The user who sent the media attachment. + * @param pagerState The state of the pager, used to navigate to items when the user selects them. + */ + @Composable + private fun MediaGalleryItem( + index: Int, + attachment: Attachment, + user: User, + pagerState: PagerState, + ) { + val coroutineScope = rememberCoroutineScope() + + BoxWithConstraints( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clickable { + coroutineScope.launch { + mediaGalleryPreviewViewModel.toggleGallery(isShowingGallery = false) + pagerState.animateScrollToPage(index) + } + }, + contentAlignment = Alignment.Center + ) { + val painter = rememberStreamImagePainter(attachment.imagePreviewUrl) + + Image( + modifier = Modifier.fillMaxSize(), + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop + ) + + UserAvatar( + modifier = Modifier + .align(Alignment.TopStart) + .padding(8.dp) + .size(24.dp), + user = user + ) + + if (attachment.type == AttachmentType.VIDEO) { + PlayButton( + modifier = Modifier + .shadow(10.dp, shape = CircleShape) + .background(color = Color.White, shape = CircleShape) + .size( + width = this@BoxWithConstraints.maxWidth / 6, + height = this@BoxWithConstraints.maxHeight / 6 + ) + ) + } + } + } + + public companion object { + /** + * The column count used for the image gallery. + */ + private const val ColumnCount = 3 + + /** + * Represents the key for the ID of the message with the attachments we're browsing. + */ + private const val KeyMessageId: String = "messageId" + + /** + * Represents the key for the starting attachment position based on the clicked attachment. + */ + private const val KeyAttachmentPosition: String = "attachmentPosition" + + /** + * Represents the key for the result of the preview, like scrolling to the message. + */ + public const val KeyMediaGalleryPreviewResult: String = "mediaGalleryPreviewResult" + + /** + * Time period inside which two taps are registered as double tap. + */ + private const val DoubleTapTimeoutMs: Long = 500L + + /** + * Maximum scale that can be applied to the image. + */ + private const val MaxZoomScale: Float = 3f + + /** + * Middle scale value that can be applied to image. + */ + private const val MidZoomScale: Float = 2f + + /** + * Default (min) value that can be applied to image. + */ + private const val DefaultZoomScale: Float = 1f + + /** + * Used to build an [Intent] to start the [MediaGalleryPreviewActivity] with the required data. + * + * @param context The context to start the activity with. + * @param messageId The ID of the message to explore the media of. + * @param attachmentPosition The initial position of the clicked media attachment. + */ + public fun getIntent(context: Context, messageId: String, attachmentPosition: Int): Intent { + return Intent(context, MediaGalleryPreviewActivity::class.java).apply { + putExtra(KeyMessageId, messageId) + putExtra(KeyAttachmentPosition, attachmentPosition) + } + } + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt new file mode 100644 index 00000000000..2ba8b423cc6 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.attachments.preview + +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult + +/** + * The contract used to start the [ImagePreviewActivity] given a message ID and the position of the clicked attachment. + */ +public class MediaGalleryPreviewContract : ActivityResultContract() { + + /** + * Creates the intent to start the [MediaGalleryPreviewActivity]. It receives a data pair of a [String] and an [Int] that + * represent the messageId and the attachmentPosition. + * + * @return The [Intent] to start the [MediaGalleryPreviewActivity]. + */ + override fun createIntent(context: Context, input: Input): Intent { + return MediaGalleryPreviewActivity.getIntent( + context, + messageId = input.messageId, + attachmentPosition = input.initialPosition + ) + } + + /** + * We parse the result as [MediaGalleryPreviewResult], which can be null in case there is no result to return. + * + * @return The [MediaGalleryPreviewResult] or null if it doesn't exist. + */ + override fun parseResult(resultCode: Int, intent: Intent?): MediaGalleryPreviewResult? { + return intent?.getParcelableExtra(MediaGalleryPreviewActivity.KeyMediaGalleryPreviewResult) + } + + /** + * Defines the input for the [MediaGalleryPreviewContract]. + * + * @param messageId The ID of the message. + * @param initialPosition The initial position of the Image gallery, based on the clicked item. + */ + public class Input( + public val messageId: String, + public val initialPosition: Int = 0, + ) +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt new file mode 100644 index 00000000000..8e8074ffcea --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.viewmodel.mediapreview + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.client.models.Attachment +import io.getstream.chat.android.client.models.Message +import io.getstream.chat.android.client.models.User +import io.getstream.chat.android.uiutils.constant.AttachmentType +import kotlinx.coroutines.flow.StateFlow + +/** + * A ViewModel capable of loading images, playing videos. + */ +public class MediaGalleryPreviewViewModel( + private val chatClient: ChatClient, + messageId: String, +) : ViewModel() { + + /** + * The currently logged in user. + */ + public val user: StateFlow = chatClient.clientState.user + + /** + * Represents the message that we observe to show the UI data. + */ + public var message: Message by mutableStateOf(Message()) + private set + + /** + * Shows or hides the media options menu and overlay in the UI. + */ + public var isShowingOptions: Boolean by mutableStateOf(false) + private set + + /** + * Shows or hides the media gallery menu in the UI. + */ + public var isShowingGallery: Boolean by mutableStateOf(false) + private set + + /** + * Loads the message data, which then updates the UI state to show media. + */ + init { + chatClient.getMessage(messageId).enqueue { result -> + if (result.isSuccess) { + this.message = result.data() + } + } + } + + /** + * Toggles if we're showing the media options menu. + * + * @param isShowingOptions If we need to show or hide the options. + */ + public fun toggleMediaOptions(isShowingOptions: Boolean) { + this.isShowingOptions = isShowingOptions + } + + /** + * Toggles if we're showing the gallery screen. + * + * @param isShowingGallery If we need to show or hide the gallery. + */ + public fun toggleGallery(isShowingGallery: Boolean) { + this.isShowingGallery = isShowingGallery + } + + /** + * Deletes the current media attachment from the message we're observing, if possible. + * + * This will in turn update the UI accordingly or finish this screen in case there are no more media attachments + * to show. + * + * @param currentMediaAttachment The image attachment to remove from the message we're updating. + */ + public fun deleteCurrentMediaAttachment(currentMediaAttachment: Attachment) { + val attachments = message.attachments + val numberOfAttachments = attachments.size + + if (message.text.isNotEmpty() || numberOfAttachments > 1) { + val message = message + + attachments.removeAll { + it.assetUrl == currentMediaAttachment.assetUrl || + (currentMediaAttachment.type == AttachmentType.IMAGE && it.imageUrl == currentMediaAttachment.imageUrl) + } + + chatClient.updateMessage(message).enqueue() + } else if (message.text.isEmpty() && numberOfAttachments == 1) { + chatClient.deleteMessage(message.id).enqueue { result -> + if (result.isSuccess) { + message = result.data() + } + } + } + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModelFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModelFactory.kt new file mode 100644 index 00000000000..0c6d2cdf334 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModelFactory.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.viewmodel.mediapreview + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.getstream.chat.android.client.ChatClient + +/** + * Holds the dependencies required for the Media Preview Screen. + * Currently builds the [MediaGalleryPreviewViewModel] using those dependencies. + */ +public class MediaGalleryPreviewViewModelFactory( + private val chatClient: ChatClient, + private val messageId: String, +) : ViewModelProvider.Factory { + + /** + * Creates a new instance of [MediaGalleryPreviewViewModel] class. + */ + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return MediaGalleryPreviewViewModel(chatClient, messageId) as T + } +} diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index 4ac77ccd5b2..8c9a4bbfae3 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -140,6 +140,7 @@ Read storage permission missing! Give permission +%1$d + +%1$d Share Image GIPHY "%1$s / %2$s" @@ -158,6 +159,15 @@ Share Photos + + Media options + Reply + Show in chat + Save media + Delete + Share + Photos + Channel item Message item From 0463a5e45e1df990c0f35a45ebb463f167c44067 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 25 Aug 2022 16:59:05 +0200 Subject: [PATCH 14/69] [3369] Create a breaking change that allows the users to open the `MediaGalleryPreviewActivity` and receives results from it instead of `ImagePreviewActivity` --- CHANGELOG.md | 5 + DEPRECATIONS.md | 2 + .../compose/sample/ui/MessagesActivity.kt | 9 +- .../api/stream-chat-android-compose.api | 92 ++++++++++++++++++- .../messages/attachments/AttachmentState.kt | 41 ++++++++- .../content/MediaAttachmentContent.kt | 16 +++- .../content/MessageAttachmentsContent.kt | 8 +- .../content/QuotedMessageAttachmentContent.kt | 8 +- .../factory/MediaAttachmentFactory.kt | 5 +- .../messages/GiphyMessageContent.kt | 2 +- .../ui/components/messages/MessageContent.kt | 14 +-- .../compose/ui/messages/MessagesScreen.kt | 8 +- .../ui/messages/list/MessageContainer.kt | 14 +-- .../compose/ui/messages/list/MessageItem.kt | 32 +++---- .../compose/ui/messages/list/MessageList.kt | 26 +++--- .../MediaGalleryPreviewViewModel.kt | 52 ++++++++++- .../MediaGalleryPreviewViewModelFactory.kt | 14 ++- 17 files changed, 270 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e892c429edf..17d287a6538 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,11 @@ ### ✅ Added ### ⚠️ Changed +- 🚨 Breaking change: `MessageAttachmentsContent` function parameter `onImagePreviewResult: (ImagePreviewResult?) -> Unit` has been replaced with `onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit`. Functionally `ImagePreviewResult` and `MediaGalleryPreviewResult` are the same, the only difference is the activity they are returned from so changes should be minimal. +- 🚨 Breaking change: `QuotedMessageAttachmentContent` function parameter `onImagePreviewResult: (ImagePreviewResult?) -> Unit` has been replaced with `onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit`. Functionally `ImagePreviewResult` and `MediaGalleryPreviewResult` are the same, the only difference is the activity they are returned from so changes should be minimal. +- 🚨 Breaking change: `MessageContent` function parameter `onImagePreviewResult: (ImagePreviewResult?) -> Unit` has been replaced with `onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit`. Functionally `ImagePreviewResult` and `MediaGalleryPreviewResult` are the same, the only difference is the activity they are returned from so changes should be minimal. +- 🚨 Breaking change: `MessageContainer` function parameter `onImagePreviewResult: (ImagePreviewResult?) -> Unit` has been replaced with `onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit`. Functionally `ImagePreviewResult` and `MediaGalleryPreviewResult` are the same, the only difference is the activity they are returned from so changes should be minimal. +- 🚨 Breaking change: Both bound (with `MessageListViewModel` as a parameter) and unbound `MessageList` Composable functions have had parameter `onImagePreviewResult: (ImagePreviewResult?) -> Unit` replaced with `onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit`. Functionally `ImagePreviewResult` and `MediaGalleryPreviewResult` are the same, the only difference is the activity they are returned from so changes should be minimal. ### ❌ Removed diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 41533a7788f..5a367452311 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -4,6 +4,8 @@ This document lists deprecated constructs in the SDK, with their expected time | API / Feature | Deprecated (warning) | Deprecated (error) | Removed | Notes | | --- | --- | --- | --- | --- | +| Lambda parameter `AttachmentState.onImagePreviewResult` | 2022.09.17
5.8.2 | 2022.10.01 ⌛ | 2022.10.15 ⌛ | Replace it with lambda parameter `AttachmentState.onMediaGalleryPreviewResult` | +| `AttachmentState` constructor containing parameter `onImagePreviewResult` | 2022.09.17
5.8.2 | 2022.10.01 ⌛ | 2022.10.15 ⌛ | This constructor has been deprecated. Use the constructor that does not contain the parameter `onImagePreviewResult`. | | `StreamDimens` constructor containing parameter `attachmentsContentImageHeight` | 2022.08.16
5.8.0 | 2022.08.30 ⌛ | 2022.09.13 ⌛ | This constructor has been deprecated. Use the constructor that does not contain the parameter `attachmentsContentImageHeight`. | | `QueryChannelsState.chatEventHandler` | 2022.08.16
5.8.0 | 2022.08.30 ⌛ | 2022.09.13 ⌛ | Use `QueryChannelsState.chatEventHandlerFactory` instead. | | Multiple event specific `BaseChatEventHandler` methods | 2022.08.16
5.8.0 | 2022.08.30 ⌛ | 2022.09.13 ⌛ | Use `handleChatEvent()` or `handleCidEvent()` instead. | diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt index e1f8ab35303..6d65edd2d6e 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt @@ -54,7 +54,7 @@ import io.getstream.chat.android.common.state.MessageMode import io.getstream.chat.android.common.state.Reply import io.getstream.chat.android.compose.sample.ChatApp import io.getstream.chat.android.compose.sample.R -import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResultType +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResultType import io.getstream.chat.android.compose.state.messages.SelectedMessageOptionsState import io.getstream.chat.android.compose.state.messages.SelectedMessageReactionsPickerState import io.getstream.chat.android.compose.state.messages.SelectedMessageReactionsState @@ -131,9 +131,10 @@ class MessagesActivity : BaseConnectedActivity() { composerViewModel.setMessageMode(MessageMode.MessageThread(message)) listViewModel.openMessageThread(message) }, - onImagePreviewResult = { result -> + // TODO edit docs + onMediaGalleryPreviewResult = { result -> when (result?.resultType) { - ImagePreviewResultType.QUOTE -> { + MediaGalleryPreviewResultType.QUOTE -> { val message = listViewModel.getMessageWithId(result.messageId) if (message != null) { @@ -141,7 +142,7 @@ class MessagesActivity : BaseConnectedActivity() { } } - ImagePreviewResultType.SHOW_IN_CHAT -> { + MediaGalleryPreviewResultType.SHOW_IN_CHAT -> { } null -> Unit } diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 8c7f896da38..8227c2b7ad7 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -190,6 +190,31 @@ public final class io/getstream/chat/android/compose/state/imagepreview/ImagePre public static fun values ()[Lio/getstream/chat/android/compose/state/imagepreview/ImagePreviewResultType; } +public final class io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult : android/os/Parcelable { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Ljava/lang/String;Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType;)V + public fun describeContents ()I + public final fun getMessageId ()Ljava/lang/String; + public final fun getResultType ()Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType; + public fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType : java/lang/Enum { + public static final field QUOTE Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType; + public static final field SHOW_IN_CHAT Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType; + public static fun valueOf (Ljava/lang/String;)Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType; + public static fun values ()[Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType; +} + public final class io/getstream/chat/android/compose/state/messageoptions/MessageOptionItemState { public static final field $stable I public synthetic fun (IJLandroidx/compose/ui/graphics/painter/Painter;JLio/getstream/chat/android/common/state/MessageAction;Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -300,15 +325,19 @@ public final class io/getstream/chat/android/compose/state/messages/attachments/ public static final field $stable I public fun (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V public synthetic fun (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/client/models/Message; public final fun component2 ()Lkotlin/jvm/functions/Function1; public final fun component3 ()Lkotlin/jvm/functions/Function1; - public final fun copy (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState; + public final fun component4 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState; public fun equals (Ljava/lang/Object;)Z public final fun getMessage ()Lio/getstream/chat/android/client/models/Message; public final fun getOnImagePreviewResult ()Lkotlin/jvm/functions/Function1; public final fun getOnLongItemClick ()Lkotlin/jvm/functions/Function1; + public final fun getOnMediaGalleryPreviewResult ()Lkotlin/jvm/functions/Function1; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -668,6 +697,17 @@ public final class io/getstream/chat/android/compose/ui/attachments/preview/Comp public final fun getLambda-3$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/chat/android/compose/ui/attachments/preview/ComposableSingletons$MediaGalleryPreviewActivityKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/preview/ComposableSingletons$MediaGalleryPreviewActivityKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function2; + public static field lambda-3 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-3$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; +} + public final class io/getstream/chat/android/compose/ui/attachments/preview/ComposableSingletons$MediaPreviewActivityKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/preview/ComposableSingletons$MediaPreviewActivityKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; @@ -703,6 +743,34 @@ public final class io/getstream/chat/android/compose/ui/attachments/preview/Imag public final fun getMessageId ()Ljava/lang/String; } +public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity : androidx/appcompat/app/AppCompatActivity { + public static final field $stable I + public static final field Companion Lio/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity$Companion; + public static final field KeyMediaGalleryPreviewResult Ljava/lang/String; + public fun ()V +} + +public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity$Companion { + public final fun getIntent (Landroid/content/Context;Ljava/lang/String;I)Landroid/content/Intent; +} + +public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract : androidx/activity/result/contract/ActivityResultContract { + public static final field $stable I + public fun ()V + public fun createIntent (Landroid/content/Context;Lio/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract$Input;)Landroid/content/Intent; + public synthetic fun createIntent (Landroid/content/Context;Ljava/lang/Object;)Landroid/content/Intent; + public fun parseResult (ILandroid/content/Intent;)Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult; + public synthetic fun parseResult (ILandroid/content/Intent;)Ljava/lang/Object; +} + +public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract$Input { + public static final field $stable I + public fun (Ljava/lang/String;I)V + public synthetic fun (Ljava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getInitialPosition ()I + public final fun getMessageId ()Ljava/lang/String; +} + public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity : androidx/appcompat/app/AppCompatActivity { public static final field $stable I public static final field Companion Lio/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity$Companion; @@ -1925,6 +1993,26 @@ public final class io/getstream/chat/android/compose/viewmodel/imagepreview/Imag public fun create (Ljava/lang/Class;)Landroidx/lifecycle/ViewModel; } +public final class io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel : androidx/lifecycle/ViewModel { + public static final field $stable I + public fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Ljava/lang/String;)V + public final fun deleteCurrentMediaAttachment (Lio/getstream/chat/android/client/models/Attachment;)V + public final fun getHeaderTitle ()Ljava/lang/String; + public final fun getMessage ()Lio/getstream/chat/android/client/models/Message; + public final fun getUser ()Lkotlinx/coroutines/flow/StateFlow; + public final fun isShowingGallery ()Z + public final fun isShowingOptions ()Z + public final fun toggleGallery (Z)V + public final fun toggleMediaOptions (Z)V +} + +public final class io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModelFactory : androidx/lifecycle/ViewModelProvider$Factory { + public static final field $stable I + public fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Ljava/lang/String;)V + public synthetic fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun create (Ljava/lang/Class;)Landroidx/lifecycle/ViewModel; +} + public final class io/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel : androidx/lifecycle/ViewModel { public static final field $stable I public fun (Lio/getstream/chat/android/compose/ui/util/StorageHelperWrapper;)V diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt index 62b0ed9c0d7..72599f217ef 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt @@ -26,13 +26,44 @@ import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryP * @param message Data that represents the message information. * @param onLongItemClick Handler for a long click on the message item. * @param onImagePreviewResult Handler when the user selects an action to scroll to and focus an image. - * @param onMediaPreviewResult Handler used when the user selects an action to perform from + * @param onMediaGalleryPreviewResult Handler used when the user selects an action to perform from * [io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewActivity]. */ -public data class AttachmentState( +public data class AttachmentState +@Deprecated( + message = "Constructor containing parameter 'onImagePreviewResult' has been deprecated, " + + "please use the constructor that does not have said parameter. Parameter 'onMediaPreviewResult' " + + "is the replacement for the parameter 'onImagePreviewResult' and is functionally the same.", + level = DeprecationLevel.WARNING +) +constructor( val message: Message, val onLongItemClick: (Message) -> Unit = {}, + @Deprecated( + message = "This parameter has been deprecated. Replace it with" + + "'AttachmentState.onMediaPreview'.", + level = DeprecationLevel.WARNING + ) val onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, - // TODO structure better with a deprecation process - val onMediaPreviewResult: (MediaGalleryPreviewResult?) -> Unit -) + val onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit, +) { + + /** + * Represents the state of Attachment items, used to render and add handlers required for the attachment to work. + * + * @param message Data that represents the message information. + * @param onLongItemClick Handler for a long click on the message item. + * @param onMediaGalleryPreviewResult Handler used when the user selects an action to perform from + * [io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewActivity]. + */ + public constructor( + message: Message, + onLongItemClick: (Message) -> Unit = {}, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit, + ) : this( + message = message, + onLongItemClick = onLongItemClick, + onImagePreviewResult = {}, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, + ) +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 25ce4fd2693..bb5924a0216 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -40,6 +40,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -194,7 +195,7 @@ internal fun RowScope.ShowMultipleMediaAttachments( .aspectRatio(TwiceAsTallAsIsWideRatio), verticalArrangement = Arrangement.spacedBy(gridSpacing) ) { - for (attachmentIndex in 0..3 step 2) { + for (attachmentIndex in 0 until MaximumNumberOfItemsInAGrid step 2) { if (attachmentIndex < attachmentCount) { MediaAttachmentContentItem( attachment = attachments[attachmentIndex], @@ -215,12 +216,12 @@ internal fun RowScope.ShowMultipleMediaAttachments( .aspectRatio(TwiceAsTallAsIsWideRatio), verticalArrangement = Arrangement.spacedBy(gridSpacing) ) { - for (attachmentIndex in 1..4 step 2) { + for (attachmentIndex in 1..MaximumNumberOfItemsInAGrid step 2) { if (attachmentIndex < attachmentCount) { val attachment = attachments[attachmentIndex] val isUploading = attachment.uploadState is Attachment.UploadState.InProgress - if (attachmentIndex == 3 && attachmentCount > 4) { + if (attachmentIndex == 3 && attachmentCount > MaximumNumberOfItemsInAGrid) { Box(modifier = Modifier.weight(1f)) { MediaAttachmentContentItem( attachment = attachment, @@ -291,6 +292,7 @@ internal fun MediaAttachmentContentItem( Box( modifier = modifier + .background(Color.Black) .fillMaxWidth() .combinedClickable( interactionSource = MutableInteractionSource(), @@ -338,7 +340,7 @@ internal fun PlayButton( Column { Image( modifier = Modifier - .fillMaxSize(0.8f) + .fillMaxSize(0.85f) .alignBy { measured -> // emulated offset as seen in the design specs, // otherwise the button is visibly off to the start of the screen @@ -397,3 +399,9 @@ private const val EqualDimensionsRatio = 1f * Composable when calling [Modifier.aspectRatio]. */ private const val TwiceAsTallAsIsWideRatio = 0.5f + +/** + * The maximum numbers of items that can be tiled inside + * [MediaAttachmentContent] + */ +internal const val MaximumNumberOfItemsInAGrid = 4 diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt index cb170906600..4689ca60203 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.getstream.sdk.chat.model.ModelType import io.getstream.chat.android.client.models.Message -import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResult +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult import io.getstream.chat.android.compose.state.messages.attachments.AttachmentState import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.uiutils.extension.hasLink @@ -33,13 +33,13 @@ import io.getstream.chat.android.uiutils.extension.hasLink * * @param message The message that contains the attachments. * @param onLongItemClick Handler for long item taps on this content. - * @param onImagePreviewResult Handler when the user selects a message option in the Image Preview screen. + * @param onMediaGalleryPreviewResult Handler when the user selects a message option in the Media Gallery Preview screen. */ @Composable public fun MessageAttachmentsContent( message: Message, onLongItemClick: (Message) -> Unit, - onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, ) { if (message.attachments.isNotEmpty()) { val (links, attachments) = message.attachments.partition { it.hasLink() && it.type != ModelType.attach_giphy } @@ -59,7 +59,7 @@ public fun MessageAttachmentsContent( val attachmentState = AttachmentState( message = message, onLongItemClick = onLongItemClick, - onImagePreviewResult = onImagePreviewResult + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult ) if (attachmentFactory != null) { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/QuotedMessageAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/QuotedMessageAttachmentContent.kt index 7a6a7e4cfaf..b2f1ce980c4 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/QuotedMessageAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/QuotedMessageAttachmentContent.kt @@ -19,7 +19,7 @@ package io.getstream.chat.android.compose.ui.attachments.content import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import io.getstream.chat.android.client.models.Message -import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResult +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult import io.getstream.chat.android.compose.state.messages.attachments.AttachmentState import io.getstream.chat.android.compose.ui.theme.ChatTheme @@ -29,14 +29,14 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme * @param message The message that contains the attachments. * @param modifier Modifier for styling. * @param onLongItemClick Handler for long item taps. - * @param onImagePreviewResult Handler when the user selects a message option in the Image Preview screen. + * @param onMediaGalleryPreviewResult Handler when the user selects an option in the Media Gallery Preview screen. */ @Composable public fun QuotedMessageAttachmentContent( message: Message, onLongItemClick: (Message) -> Unit, modifier: Modifier = Modifier, - onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, ) { val attachments = message.attachments @@ -56,7 +56,7 @@ public fun QuotedMessageAttachmentContent( val attachmentState = AttachmentState( message = message, onLongItemClick = onLongItemClick, - onImagePreviewResult = onImagePreviewResult + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult ) quoteAttachmentFactory?.content?.invoke( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt index f2fdc43eb70..49d83fcb865 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory +import io.getstream.chat.android.compose.ui.attachments.content.MaximumNumberOfItemsInAGrid import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentContent import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentPreviewContent import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentPreviewItemSize @@ -86,8 +87,8 @@ private fun DefaultContentPlayButton() { .shadow(10.dp, shape = CircleShape) .background(color = Color.White, shape = CircleShape) .size( - width = 42.dp, - height = 42.dp + width = ChatTheme.dimens.attachmentsContentImageWidth / MaximumNumberOfItemsInAGrid, + height = ChatTheme.dimens.attachmentsContentImageWidth / MaximumNumberOfItemsInAGrid, ) ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/GiphyMessageContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/GiphyMessageContent.kt index 33f1b256774..0c50f1f54eb 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/GiphyMessageContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/GiphyMessageContent.kt @@ -102,7 +102,7 @@ public fun GiphyMessageContent( MessageAttachmentsContent( message = message, onLongItemClick = {}, - onImagePreviewResult = {}, + onMediaGalleryPreviewResult = {}, ) Spacer( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt index 9a0971e1ad3..1fcdb608382 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResult +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult import io.getstream.chat.android.compose.state.messages.list.GiphyAction import io.getstream.chat.android.compose.ui.attachments.content.MessageAttachmentsContent import io.getstream.chat.android.compose.ui.messages.list.DefaultMessageTextContent @@ -41,7 +41,7 @@ import io.getstream.chat.android.compose.ui.util.isGiphyEphemeral * @param onLongItemClick Handler when the item is long clicked. * @param onGiphyActionClick Handler for Giphy actions. * @param onQuotedMessageClick Handler for quoted message click action. - * @param onImagePreviewResult Handler when selecting images in the default content. + * @param onMediaGalleryPreviewResult Handler when the user selects an option in the Media Gallery Preview screen. * @param giphyEphemeralContent Composable that represents the default Giphy message content. * @param deletedMessageContent Composable that represents the default content of a deleted message. * @param regularMessageContent Composable that represents the default regular message content, such as attachments and @@ -54,7 +54,7 @@ public fun MessageContent( onLongItemClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, - onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, giphyEphemeralContent: @Composable () -> Unit = { DefaultMessageGiphyContent( message = message, @@ -68,7 +68,7 @@ public fun MessageContent( DefaultMessageContent( message = message, onLongItemClick = onLongItemClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick ) }, @@ -125,21 +125,21 @@ internal fun DefaultMessageDeletedContent( * * @param message The message to show. * @param onLongItemClick Handler when the item is long clicked. - * @param onImagePreviewResult Handler when selecting images in the default content. + * @param onMediaGalleryPreviewResult Handler when the user selects an option in the Media Gallery Preview screen. * @param onQuotedMessageClick Handler for quoted message click action. */ @Composable internal fun DefaultMessageContent( message: Message, onLongItemClick: (Message) -> Unit, - onImagePreviewResult: (ImagePreviewResult?) -> Unit, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit, ) { Column { MessageAttachmentsContent( message = message, onLongItemClick = onLongItemClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, ) if (message.text.isNotEmpty()) { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt index 70fbdb6bd12..7e1cd264b73 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt @@ -65,7 +65,7 @@ import io.getstream.chat.android.common.state.MessageMode import io.getstream.chat.android.common.state.Reply import io.getstream.chat.android.common.state.Resend import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResultType +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResultType import io.getstream.chat.android.compose.state.messageoptions.MessageOptionItemState import io.getstream.chat.android.compose.state.messages.SelectedMessageFailedModerationState import io.getstream.chat.android.compose.state.messages.SelectedMessageOptionsState @@ -209,9 +209,9 @@ public fun MessagesScreen( composerViewModel.setMessageMode(MessageMode.MessageThread(message)) listViewModel.openMessageThread(message) }, - onImagePreviewResult = { result -> + onMediaGalleryPreviewResult = { result -> when (result?.resultType) { - ImagePreviewResultType.QUOTE -> { + MediaGalleryPreviewResultType.QUOTE -> { val message = listViewModel.getMessageWithId(result.messageId) if (message != null) { @@ -219,7 +219,7 @@ public fun MessagesScreen( } } - ImagePreviewResultType.SHOW_IN_CHAT -> { + MediaGalleryPreviewResultType.SHOW_IN_CHAT -> { listViewModel.focusMessage(result.messageId) } null -> Unit diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt index 26d855ad900..c34dca5fa00 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResult +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult import io.getstream.chat.android.compose.state.messages.list.DateSeparatorState import io.getstream.chat.android.compose.state.messages.list.GiphyAction import io.getstream.chat.android.compose.state.messages.list.MessageItemState @@ -51,7 +51,7 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme * @param onThreadClick Handler when the user taps on a thread within a message item. * @param onGiphyActionClick Handler when the user taps on Giphy message actions. * @param onQuotedMessageClick Handler for quoted message click action. - * @param onImagePreviewResult Handler when the user receives a result from the Image Preview. + * @param onMediaGalleryPreviewResult Handler when the user receives a result from the Media Gallery Preview. * @param dateSeparatorContent Composable that represents date separators. * @param threadSeparatorContent Composable that represents thread separators. * @param systemMessageContent Composable that represents system messages. @@ -65,7 +65,7 @@ public fun MessageContainer( onThreadClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, - onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, dateSeparatorContent: @Composable (DateSeparatorState) -> Unit = { DefaultMessageDateSeparatorContent(dateSeparator = it) }, @@ -82,7 +82,7 @@ public fun MessageContainer( onReactionsClick = onReactionsClick, onThreadClick = onThreadClick, onGiphyActionClick = onGiphyActionClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick, ) }, @@ -189,7 +189,7 @@ internal fun DefaultSystemMessageContent(systemMessageState: SystemMessageState) * @param onThreadClick Handler when the user clicks on the message thread. * @param onGiphyActionClick Handler when the user selects a Giphy action. * @param onQuotedMessageClick Handler for quoted message click action. - * @param onImagePreviewResult Handler when the user receives an image preview result. + * @param onMediaGalleryPreviewResult Handler when the user receives a result from the Media Gallery Preview. */ @Composable internal fun DefaultMessageItem( @@ -199,7 +199,7 @@ internal fun DefaultMessageItem( onThreadClick: (Message) -> Unit, onGiphyActionClick: (GiphyAction) -> Unit, onQuotedMessageClick: (Message) -> Unit, - onImagePreviewResult: (ImagePreviewResult?) -> Unit, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, ) { MessageItem( messageItem = messageItem, @@ -208,6 +208,6 @@ internal fun DefaultMessageItem( onThreadClick = onThreadClick, onGiphyActionClick = onGiphyActionClick, onQuotedMessageClick = onQuotedMessageClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt index f004160c7ed..372b4d52d91 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt @@ -55,7 +55,7 @@ import androidx.compose.ui.unit.dp import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.common.state.DeletedMessageVisibility import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResult +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult import io.getstream.chat.android.compose.state.messages.list.GiphyAction import io.getstream.chat.android.compose.state.messages.list.MessageFocused import io.getstream.chat.android.compose.state.messages.list.MessageItemGroupPosition.Bottom @@ -100,7 +100,7 @@ import io.getstream.chat.android.compose.ui.util.isUploading * @param onThreadClick Handler for thread clicks, if this message has a thread going. * @param onGiphyActionClick Handler when the user taps on an action button in a giphy message item. * @param onQuotedMessageClick Handler for quoted message click action. - * @param onImagePreviewResult Handler when the user selects an option in the Image Preview screen. + * @param onMediaGalleryPreviewResult Handler when the user selects an option in the Media Gallery Preview screen. * @param leadingContent The content shown at the start of a message list item. By default, we provide * [DefaultMessageItemLeadingContent], which shows a user avatar if the message doesn't belong to the * current user. @@ -123,7 +123,7 @@ public fun MessageItem( onThreadClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, - onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, leadingContent: @Composable RowScope.(MessageItemState) -> Unit = { DefaultMessageItemLeadingContent(messageItem = it) }, @@ -137,7 +137,7 @@ public fun MessageItem( DefaultMessageItemCenterContent( messageItem = it, onLongItemClick = onLongItemClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onGiphyActionClick = onGiphyActionClick, onQuotedMessageClick = onQuotedMessageClick, ) @@ -382,7 +382,7 @@ internal fun DefaultMessageItemTrailingContent( * @param onLongItemClick Handler when the user selects a message, on long tap. * @param onGiphyActionClick Handler when the user taps on an action button in a giphy message item. * @param onQuotedMessageClick Handler for quoted message click action. - * @param onImagePreviewResult Handler when the user selects an option in the Image Preview screen. + * @param onMediaGalleryPreviewResult Handler when the user selects an option in the Media Gallery Preview screen. */ @Composable internal fun DefaultMessageItemCenterContent( @@ -390,7 +390,7 @@ internal fun DefaultMessageItemCenterContent( onLongItemClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, - onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, ) { val modifier = Modifier.widthIn(max = ChatTheme.dimens.messageItemMaxWidth) if (messageItem.message.isEmojiOnlyWithoutBubble()) { @@ -399,7 +399,7 @@ internal fun DefaultMessageItemCenterContent( messageItem = messageItem, onLongItemClick = onLongItemClick, onGiphyActionClick = onGiphyActionClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick ) } else { @@ -408,7 +408,7 @@ internal fun DefaultMessageItemCenterContent( messageItem = messageItem, onLongItemClick = onLongItemClick, onGiphyActionClick = onGiphyActionClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick ) } @@ -422,7 +422,7 @@ internal fun DefaultMessageItemCenterContent( * @param onLongItemClick Handler when the user selects a message, on long tap. * @param onGiphyActionClick Handler when the user taps on an action button in a giphy message item. * @param onQuotedMessageClick Handler for quoted message click action. - * @param onImagePreviewResult Handler when the user selects an option in the Image Preview screen. + * @param onMediaGalleryPreviewResult Handler used when the user selects an option in the Media Gallery Preview screen. */ @Composable internal fun EmojiMessageContent( @@ -431,7 +431,7 @@ internal fun EmojiMessageContent( onLongItemClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, - onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, ) { val message = messageItem.message @@ -440,7 +440,7 @@ internal fun EmojiMessageContent( message = message, onLongItemClick = onLongItemClick, onGiphyActionClick = onGiphyActionClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick ) } else { @@ -449,7 +449,7 @@ internal fun EmojiMessageContent( message = message, onLongItemClick = onLongItemClick, onGiphyActionClick = onGiphyActionClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick ) @@ -473,7 +473,7 @@ internal fun EmojiMessageContent( * @param onLongItemClick Handler when the user selects a message, on long tap. * @param onGiphyActionClick Handler when the user taps on an action button in a giphy message item. * @param onQuotedMessageClick Handler for quoted message click action. - * @param onImagePreviewResult Handler when the user selects an option in the Image Preview screen. + * @param onMediaGalleryPreviewResult Handler when the user selects an option in the Media Gallery Preview screen. */ @Composable internal fun RegularMessageContent( @@ -482,7 +482,7 @@ internal fun RegularMessageContent( onLongItemClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, - onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, ) { val (message, position, _, ownsMessage, _) = messageItem @@ -511,7 +511,7 @@ internal fun RegularMessageContent( message = message, onLongItemClick = onLongItemClick, onGiphyActionClick = onGiphyActionClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick ) } @@ -527,7 +527,7 @@ internal fun RegularMessageContent( message = message, onLongItemClick = onLongItemClick, onGiphyActionClick = onGiphyActionClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt index 44e9c35714e..c512fc28e20 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.compose.R -import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResult -import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResultType +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResultType import io.getstream.chat.android.compose.state.messages.MessagesState import io.getstream.chat.android.compose.state.messages.list.GiphyAction import io.getstream.chat.android.compose.state.messages.list.MessageListItemState @@ -61,7 +61,7 @@ import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel * @param onScrollToBottom Handler when the user reaches the bottom. * @param onGiphyActionClick Handler when the user clicks on a giphy action such as shuffle, send or cancel. * @param onQuotedMessageClick Handler for quoted message click action. - * @param onImagePreviewResult Handler when the user selects an option in the Image Preview screen. + * @param onMediaGalleryPreviewResult Handler when the user selects an option in the Media Gallery Preview screen. * @param loadingContent Composable that represents the loading content, when we're loading the initial data. * @param emptyContent Composable that represents the empty content if there are no messages. * @param helperContent Composable that, by default, represents the helper content featuring scrolling behavior based @@ -86,8 +86,8 @@ public fun MessageList( onScrollToBottom: () -> Unit = { viewModel.clearNewMessageState() }, onGiphyActionClick: (GiphyAction) -> Unit = { viewModel.performGiphyAction(it) }, onQuotedMessageClick: (Message) -> Unit = { viewModel.scrollToSelectedMessage(it) }, - onImagePreviewResult: (ImagePreviewResult?) -> Unit = { - if (it?.resultType == ImagePreviewResultType.SHOW_IN_CHAT) { + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = { + if (it?.resultType == MediaGalleryPreviewResultType.SHOW_IN_CHAT) { viewModel.focusMessage(it.messageId) } }, @@ -103,7 +103,7 @@ public fun MessageList( itemContent: @Composable (MessageListItemState) -> Unit = { messageListItem -> DefaultMessageContainer( messageListItem = messageListItem, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onThreadClick = onThreadClick, onLongItemClick = onLongItemClick, onReactionsClick = onReactionsClick, @@ -122,7 +122,7 @@ public fun MessageList( onLongItemClick = onLongItemClick, onReactionsClick = onReactionsClick, onScrolledToBottom = onScrollToBottom, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, itemContent = itemContent, helperContent = helperContent, loadingMoreContent = loadingMoreContent, @@ -136,7 +136,7 @@ public fun MessageList( * The default message container item. * * @param messageListItem The state of the message list item. - * @param onImagePreviewResult Handler when the user receives a result from the Image Preview. + * @param onMediaGalleryPreviewResult Handler when the user receives a result from the Media Gallery Preview. * @param onThreadClick Handler when the user taps on a thread within a message item. * @param onLongItemClick Handler when the user long taps on an item. * @param onReactionsClick Handler when the user taps on message reactions. @@ -146,7 +146,7 @@ public fun MessageList( @Composable internal fun DefaultMessageContainer( messageListItem: MessageListItemState, - onImagePreviewResult: (ImagePreviewResult?) -> Unit, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, onThreadClick: (Message) -> Unit, onLongItemClick: (Message) -> Unit, onReactionsClick: (Message) -> Unit = {}, @@ -159,7 +159,7 @@ internal fun DefaultMessageContainer( onReactionsClick = onReactionsClick, onThreadClick = onThreadClick, onGiphyActionClick = onGiphyActionClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick, ) } @@ -210,7 +210,7 @@ internal fun DefaultMessageListEmptyContent(modifier: Modifier) { * @param onThreadClick Handler for when the user taps on a message with an active thread. * @param onLongItemClick Handler for when the user long taps on an item. * @param onReactionsClick Handler when the user taps on message reactions and selects them. - * @param onImagePreviewResult Handler when the user selects an option in the Image Preview screen. + * @param onMediaGalleryPreviewResult Handler when the user selects an option in the Media Gallery Preview screen. * @param onGiphyActionClick Handler when the user clicks on a giphy action such as shuffle, send or cancel. * @param onQuotedMessageClick Handler for quoted message click action. * @param loadingContent Composable that represents the loading content, when we're loading the initial data. @@ -233,7 +233,7 @@ public fun MessageList( onThreadClick: (Message) -> Unit = {}, onLongItemClick: (Message) -> Unit = {}, onReactionsClick: (Message) -> Unit = {}, - onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, loadingContent: @Composable () -> Unit = { DefaultMessageListLoadingIndicator(modifier) }, @@ -249,7 +249,7 @@ public fun MessageList( onThreadClick = onThreadClick, onReactionsClick = onReactionsClick, onGiphyActionClick = onGiphyActionClick, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onQuotedMessageClick = onQuotedMessageClick, ) }, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt index 8e8074ffcea..1a6f054fabd 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt @@ -20,19 +20,25 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.models.Attachment +import io.getstream.chat.android.client.models.ConnectionState import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.client.models.User +import io.getstream.chat.android.client.setup.state.ClientState +import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider import io.getstream.chat.android.uiutils.constant.AttachmentType import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch /** * A ViewModel capable of loading images, playing videos. */ public class MediaGalleryPreviewViewModel( private val chatClient: ChatClient, - messageId: String, + private val clientState: ClientState, + private val messageId: String, ) : ViewModel() { /** @@ -46,6 +52,12 @@ public class MediaGalleryPreviewViewModel( public var message: Message by mutableStateOf(Message()) private set + /** + * Represent the header title of the gallery screen. + */ + public var headerTitle: String by mutableStateOf("") + private set + /** * Shows or hides the media options menu and overlay in the UI. */ @@ -62,13 +74,45 @@ public class MediaGalleryPreviewViewModel( * Loads the message data, which then updates the UI state to show media. */ init { - chatClient.getMessage(messageId).enqueue { result -> - if (result.isSuccess) { - this.message = result.data() + viewModelScope.launch(DispatcherProvider.IO) { + fetchMessage() + observeConnectionStateChanges() + } + } + + /** + * Fetches the message according to the message ID. + */ + private suspend fun fetchMessage() { + val result = chatClient.getMessage(messageId).await() + + if (result.isSuccess) { + this.message = result.data() + headerTitle = message.user.name + } + } + + /** + * Attempts to fetch the message again if it was not + * successfully fetched the previous time + */ + private suspend fun observeConnectionStateChanges() { + clientState.connectionState.collect { connectionState -> + // TODO finish + when (connectionState) { + ConnectionState.CONNECTED -> onConnected() + ConnectionState.CONNECTING -> {} + ConnectionState.OFFLINE -> {} } } } + private suspend fun onConnected() { + if (message.id.isEmpty()) { + fetchMessage() + } + } + /** * Toggles if we're showing the media options menu. * diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModelFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModelFactory.kt index 0c6d2cdf334..8ca32e4f296 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModelFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModelFactory.kt @@ -19,13 +19,21 @@ package io.getstream.chat.android.compose.viewmodel.mediapreview import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.client.setup.state.ClientState /** * Holds the dependencies required for the Media Preview Screen. * Currently builds the [MediaGalleryPreviewViewModel] using those dependencies. + * + * @param chatClient An instance of the low level client + * used for basic chat API functionality. + * @param clientState Holds information about the current SDK state. + * @param messageId The ID of the message we are fetching in order + * to display the attachments. */ public class MediaGalleryPreviewViewModelFactory( private val chatClient: ChatClient, + private val clientState: ClientState = chatClient.clientState, private val messageId: String, ) : ViewModelProvider.Factory { @@ -34,6 +42,10 @@ public class MediaGalleryPreviewViewModelFactory( */ override fun create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") - return MediaGalleryPreviewViewModel(chatClient, messageId) as T + return MediaGalleryPreviewViewModel( + chatClient = chatClient, + clientState = clientState, + messageId = messageId + ) as T } } From d4dc933f0d8d27b264a178d48197bebc653ae9a8 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 25 Aug 2022 17:12:36 +0200 Subject: [PATCH 15/69] [3369] Fix max line violations --- .../ui/attachments/content/MessageAttachmentsContent.kt | 3 ++- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 7 ++++++- .../ui/attachments/preview/MediaGalleryPreviewContract.kt | 7 ++++--- .../viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt | 3 ++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt index 4689ca60203..308d706fa00 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt @@ -33,7 +33,8 @@ import io.getstream.chat.android.uiutils.extension.hasLink * * @param message The message that contains the attachments. * @param onLongItemClick Handler for long item taps on this content. - * @param onMediaGalleryPreviewResult Handler when the user selects a message option in the Media Gallery Preview screen. + * @param onMediaGalleryPreviewResult Handler when the user selects a message option in the + * Media Gallery Preview screen. */ @Composable public fun MessageAttachmentsContent( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index c4cfd8f5606..bf9b866eefa 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -484,7 +484,12 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { ) } is Reply -> { - handleResult(MediaGalleryPreviewResult(messageId = message.id, resultType = MediaGalleryPreviewResultType.QUOTE)) + handleResult( + MediaGalleryPreviewResult( + messageId = message.id, + resultType = MediaGalleryPreviewResultType.QUOTE + ) + ) } is Delete -> mediaGalleryPreviewViewModel.deleteCurrentMediaAttachment(message.attachments[currentPage]) is SaveMedia -> { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt index 2ba8b423cc6..457dc10e69f 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt @@ -24,11 +24,12 @@ import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryP /** * The contract used to start the [ImagePreviewActivity] given a message ID and the position of the clicked attachment. */ -public class MediaGalleryPreviewContract : ActivityResultContract() { +public class MediaGalleryPreviewContract : + ActivityResultContract() { /** - * Creates the intent to start the [MediaGalleryPreviewActivity]. It receives a data pair of a [String] and an [Int] that - * represent the messageId and the attachmentPosition. + * Creates the intent to start the [MediaGalleryPreviewActivity]. + * It receives a data pair of a [String] and an [Int] that represent the messageId and the attachmentPosition. * * @return The [Intent] to start the [MediaGalleryPreviewActivity]. */ diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt index 1a6f054fabd..f22b79e40e9 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt @@ -148,7 +148,8 @@ public class MediaGalleryPreviewViewModel( attachments.removeAll { it.assetUrl == currentMediaAttachment.assetUrl || - (currentMediaAttachment.type == AttachmentType.IMAGE && it.imageUrl == currentMediaAttachment.imageUrl) + (currentMediaAttachment.type == AttachmentType.IMAGE && + it.imageUrl == currentMediaAttachment.imageUrl) } chatClient.updateMessage(message).enqueue() From 219e5b0c5699b3227cf63c7675218093c2dce43a Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 25 Aug 2022 17:12:57 +0200 Subject: [PATCH 16/69] [3369] Fix max line violations --- .../viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt index f22b79e40e9..2effaa01184 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt @@ -148,8 +148,10 @@ public class MediaGalleryPreviewViewModel( attachments.removeAll { it.assetUrl == currentMediaAttachment.assetUrl || - (currentMediaAttachment.type == AttachmentType.IMAGE && - it.imageUrl == currentMediaAttachment.imageUrl) + ( + currentMediaAttachment.type == AttachmentType.IMAGE && + it.imageUrl == currentMediaAttachment.imageUrl + ) } chatClient.updateMessage(message).enqueue() From f78f52d73eeeb523efdd6885f84e5ca7f819bebc Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 25 Aug 2022 17:14:46 +0200 Subject: [PATCH 17/69] [3369] Generate detekt baseline. --- stream-chat-android-compose/detekt-baseline.xml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index 81810c8bc8c..f9afea66336 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -9,17 +9,18 @@ ComplexMethod:MessageOptions.kt$@Composable public fun defaultMessageOptionsState( selectedMessage: Message, currentUser: User?, isInThread: Boolean, ownCapabilities: Set<String>, ): List<MessageOptionItemState> ForbiddenComment:MessageText.kt$// TODO: Fix emoji font padding once this is resolved and exposed: https://issuetracker.google.com/issues/171394808 ForbiddenComment:QuotedMessageText.kt$// TODO: Fix emoji font padding once this is resolved and exposed: https://issuetracker.google.com/issues/171394808 + LargeClass:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity : AppCompatActivity LongMethod:GiphyMessageContent.kt$@Composable public fun GiphyMessageContent( message: Message, modifier: Modifier = Modifier, onGiphyActionClick: (GiphyAction) -> Unit = {}, ) LongMethod:GroupAvatar.kt$@Composable public fun GroupAvatar( users: List<User>, modifier: Modifier = Modifier, shape: Shape = ChatTheme.shapes.avatar, textStyle: TextStyle = ChatTheme.typography.captionBold, onClick: (() -> Unit)? = null, ) LongMethod:ImageAttachmentContent.kt$@OptIn(ExperimentalFoundationApi::class) @Composable public fun ImageAttachmentContent( attachmentState: AttachmentState, modifier: Modifier = Modifier, ) + LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun ImagePreviewContent( attachment: Attachment, pagerState: PagerState, page: Int, ) + LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun VideoPreviewContent( attachment: Attachment, pagerState: PagerState, page: Int, onPlaybackError: () -> Unit, ) LongMethod:MessageComposer.kt$@Composable internal fun DefaultComposerIntegrations( messageInputState: MessageComposerState, onAttachmentsClick: () -> Unit, onCommandsClick: () -> Unit, ownCapabilities: Set<String>, ) LongMethod:MessageListViewModel.kt$MessageListViewModel$private fun observeChannel() LongMethod:MessageOptions.kt$@Composable public fun defaultMessageOptionsState( selectedMessage: Message, currentUser: User?, isInThread: Boolean, ownCapabilities: Set<String>, ): List<MessageOptionItemState> LongMethod:Messages.kt$@Composable public fun Messages( messagesState: MessagesState, lazyListState: LazyListState, onMessagesStartReached: () -> Unit, onLastVisibleMessageChanged: (Message) -> Unit, onScrolledToBottom: () -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(vertical = 16.dp), helperContent: @Composable BoxScope.() -> Unit = { DefaultMessagesHelperContent(messagesState, lazyListState) }, loadingMoreContent: @Composable () -> Unit = { DefaultMessagesLoadingMoreIndicator() }, itemContent: @Composable (MessageListItemState) -> Unit, ) LongMethod:StreamTypography.kt$StreamTypography.Companion$public fun defaultTypography(fontFamily: FontFamily? = null): StreamTypography LongParameterList:MessageComposer.kt$( value: String, coolDownTime: Int, attachments: List<Attachment>, validationErrors: List<ValidationError>, ownCapabilities: Set<String>, onSendMessage: (String, List<Attachment>) -> Unit, ) - LongParameterList:MessageContainer.kt$( messageItem: MessageItemState, onLongItemClick: (Message) -> Unit, onReactionsClick: (Message) -> Unit = {}, onThreadClick: (Message) -> Unit, onGiphyActionClick: (GiphyAction) -> Unit, onQuotedMessageClick: (Message) -> Unit, onImagePreviewResult: (ImagePreviewResult?) -> Unit, ) - LongParameterList:MessageList.kt$( messageListItem: MessageListItemState, onImagePreviewResult: (ImagePreviewResult?) -> Unit, onThreadClick: (Message) -> Unit, onLongItemClick: (Message) -> Unit, onReactionsClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit, onQuotedMessageClick: (Message) -> Unit, ) LongParameterList:Messages.kt$( messagesState: MessagesState, lazyListState: LazyListState, onMessagesStartReached: () -> Unit, onLastVisibleMessageChanged: (Message) -> Unit, onScrolledToBottom: () -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(vertical = 16.dp), helperContent: @Composable BoxScope.() -> Unit = { DefaultMessagesHelperContent(messagesState, lazyListState) }, loadingMoreContent: @Composable () -> Unit = { DefaultMessagesLoadingMoreIndicator() }, itemContent: @Composable (MessageListItemState) -> Unit, ) LongParameterList:MessagesScreen.kt$( context: Context, channelId: String, enforceUniqueReactions: Boolean, messageLimit: Int, showDateSeparators: Boolean, showSystemMessages: Boolean, deletedMessageVisibility: DeletedMessageVisibility, messageFooterVisibility: MessageFooterVisibility, ) MagicNumber:AvatarPosition.kt$3 @@ -28,10 +29,12 @@ MagicNumber:ImageAttachmentContent.kt$3 MagicNumber:ImageAttachmentContent.kt$4 MagicNumber:ImagePreviewActivity.kt$ImagePreviewActivity$8f + MagicNumber:MediaAttachmentContent.kt$0.85f MagicNumber:MediaAttachmentContent.kt$3 - MagicNumber:MediaAttachmentContent.kt$4 MagicNumber:MediaAttachmentContent.kt$8 MagicNumber:MediaAttachmentFactory.kt$4 + MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$6 + MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$8f MagicNumber:Messages.kt$3 MagicNumber:Messages.kt$5 MagicNumber:SearchInput.kt$8f @@ -43,5 +46,8 @@ MaxLineLength:MessagesScreen.kt$* MaxLineLength:MessagesViewModelFactory.kt$MessagesViewModelFactory$private val dateSeparatorThresholdMillis: Long = TimeUnit.HOURS.toMillis(MessageListViewModel.DateSeparatorDefaultHourThreshold) ReturnCount:MessageListViewModel.kt$MessageListViewModel$public fun updateLastSeenMessage(message: Message) + TooManyFunctions:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity : AppCompatActivity + UnusedPrivateMember:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$private fun shareVideo(videoUri: Uri) + UnusedPrivateMember:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$videoUri: Uri From bae0d9e1f3fb0049a905a12835202ea9abb91ce8 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Fri, 26 Aug 2022 18:20:24 +0200 Subject: [PATCH 18/69] [3369] Start refactoring `MediaGalleryPreviewActivity` to support offline mode --- .../api/stream-chat-android-compose.api | 2 +- .../preview/MediaGalleryPreviewActivity.kt | 104 ++++++++++++------ .../MediaGalleryPreviewViewModel.kt | 16 +-- 3 files changed, 78 insertions(+), 44 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 8227c2b7ad7..ac95e869cf5 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1997,7 +1997,7 @@ public final class io/getstream/chat/android/compose/viewmodel/mediapreview/Medi public static final field $stable I public fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Ljava/lang/String;)V public final fun deleteCurrentMediaAttachment (Lio/getstream/chat/android/client/models/Attachment;)V - public final fun getHeaderTitle ()Ljava/lang/String; + public final fun getConnectionState ()Lio/getstream/chat/android/client/models/ConnectionState; public final fun getMessage ()Lio/getstream/chat/android/client/models/Message; public final fun getUser ()Lkotlinx/coroutines/flow/StateFlow; public final fun isShowingGallery ()Z diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index bf9b866eefa..6852ae3efed 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -122,6 +122,7 @@ import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.rememberPagerState import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.models.Attachment +import io.getstream.chat.android.client.models.ConnectionState import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.client.models.User import io.getstream.chat.android.compose.R @@ -137,6 +138,7 @@ import io.getstream.chat.android.compose.state.mediagallerypreview.SaveMedia import io.getstream.chat.android.compose.state.mediagallerypreview.ShowInChat import io.getstream.chat.android.compose.ui.attachments.content.PlayButton import io.getstream.chat.android.compose.ui.components.LoadingIndicator +import io.getstream.chat.android.compose.ui.components.NetworkLoadingIndicator import io.getstream.chat.android.compose.ui.components.Timestamp import io.getstream.chat.android.compose.ui.components.avatar.UserAvatar import io.getstream.chat.android.compose.ui.theme.ChatTheme @@ -193,9 +195,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { return@ChatTheme } - if (message.id.isNotEmpty()) { - MediaGalleryPreviewContentWrapper(message, attachmentPosition) - } + MediaGalleryPreviewContentWrapper(message, attachmentPosition) } } } @@ -224,15 +224,22 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { modifier = Modifier.fillMaxSize(), topBar = { MediaGalleryPreviewTopBar(message) }, content = { contentPadding -> - Surface( - modifier = Modifier - .fillMaxSize() - .padding(contentPadding) - ) { - MediaPreviewContent(pagerState, message.attachments) + if (message.id.isNotEmpty()) { + + Surface( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + ) { + MediaPreviewContent(pagerState, message.attachments) + } } }, - bottomBar = { MediaGalleryPreviewBottomBar(message.attachments, pagerState) } + bottomBar = { + if (message.id.isNotEmpty()) { + MediaGalleryPreviewBottomBar(message.attachments, pagerState) + } + } ) AnimatedVisibility( @@ -250,18 +257,20 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { ) } - AnimatedVisibility( - visible = mediaGalleryPreviewViewModel.isShowingGallery, - enter = fadeIn(), - exit = fadeOut() - ) { - MediaGallery( - pagerState = pagerState, - modifier = Modifier.animateEnterExit( - enter = slideInVertically(initialOffsetY = { height -> height / 2 }), - exit = slideOutVertically(targetOffsetY = { height -> height / 2 }) + if (message.id.isNotEmpty()) { + AnimatedVisibility( + visible = mediaGalleryPreviewViewModel.isShowingGallery, + enter = fadeIn(), + exit = fadeOut() + ) { + MediaGallery( + pagerState = pagerState, + modifier = Modifier.animateEnterExit( + enter = slideInVertically(initialOffsetY = { height -> height / 2 }), + exit = slideOutVertically(targetOffsetY = { height -> height / 2 }) + ) ) - ) + } } } } @@ -299,7 +308,10 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { message = message ) - MediaGalleryPreviewOptionsToggle(modifier = Modifier.weight(1f)) + MediaGalleryPreviewOptionsToggle( + modifier = Modifier.weight(1f), + message = message + ) } } } @@ -320,11 +332,25 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - Text( - text = message.user.name, - style = ChatTheme.typography.title3Bold, - color = ChatTheme.colors.textHighEmphasis - ) + val textStyle = ChatTheme.typography.title3Bold + val textColor = ChatTheme.colors.textHighEmphasis + + when (mediaGalleryPreviewViewModel.connectionState) { + ConnectionState.CONNECTED -> Text( + text = message.user.name, + style = textStyle, + color = textColor + ) + ConnectionState.CONNECTING -> NetworkLoadingIndicator( + textStyle = textStyle, + textColor = textColor + ) + ConnectionState.OFFLINE -> Text( + text = getString(R.string.stream_compose_disconnected), + style = textStyle, + color = textColor + ) + } Timestamp(date = message.updatedAt ?: message.createdAt ?: Date()) } @@ -337,6 +363,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { */ @Composable private fun MediaGalleryPreviewOptionsToggle( + message: Message, modifier: Modifier = Modifier, ) { Icon( @@ -345,11 +372,12 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false), - onClick = { mediaGalleryPreviewViewModel.toggleMediaOptions(isShowingOptions = true) } + onClick = { mediaGalleryPreviewViewModel.toggleMediaOptions(isShowingOptions = true) }, + enabled = message.id.isNotEmpty() ), painter = painterResource(id = R.drawable.stream_compose_ic_menu_vertical), contentDescription = stringResource(R.string.stream_compose_image_options), - tint = ChatTheme.colors.textHighEmphasis + tint = if (message.id.isNotEmpty()) ChatTheme.colors.textHighEmphasis else ChatTheme.colors.disabled ) } @@ -924,6 +952,9 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { @Composable private fun defaultMediaOptions(message: Message): List { val user by mediaGalleryPreviewViewModel.user.collectAsState() + val saveMediaColor = + if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED) ChatTheme.colors.textHighEmphasis else ChatTheme.colors.disabled + val options = mutableListOf( MediaGalleryPreviewOption( title = stringResource(id = R.string.stream_compose_media_gallery_preview_reply), @@ -941,21 +972,24 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { ), MediaGalleryPreviewOption( title = stringResource(id = R.string.stream_compose_media_gallery_preview_save_image), - titleColor = ChatTheme.colors.textHighEmphasis, + titleColor = saveMediaColor, iconPainter = painterResource(id = R.drawable.stream_compose_ic_download), - iconColor = ChatTheme.colors.textHighEmphasis, - action = SaveMedia(message) + iconColor = saveMediaColor, + action = SaveMedia(message), ) ) if (message.user.id == user?.id) { + val deleteColor = + if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED) ChatTheme.colors.errorAccent else ChatTheme.colors.disabled + options.add( MediaGalleryPreviewOption( title = stringResource(id = R.string.stream_compose_media_gallery_preview_delete), - titleColor = ChatTheme.colors.errorAccent, + titleColor = deleteColor, iconPainter = painterResource(id = R.drawable.stream_compose_ic_delete), - iconColor = ChatTheme.colors.errorAccent, - action = Delete(message) + iconColor = deleteColor, + action = Delete(message), ) ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt index 2effaa01184..673d22b29e1 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt @@ -27,7 +27,6 @@ import io.getstream.chat.android.client.models.ConnectionState import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.client.models.User import io.getstream.chat.android.client.setup.state.ClientState -import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider import io.getstream.chat.android.uiutils.constant.AttachmentType import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -55,7 +54,7 @@ public class MediaGalleryPreviewViewModel( /** * Represent the header title of the gallery screen. */ - public var headerTitle: String by mutableStateOf("") + public var connectionState: ConnectionState by mutableStateOf(ConnectionState.OFFLINE) private set /** @@ -74,7 +73,7 @@ public class MediaGalleryPreviewViewModel( * Loads the message data, which then updates the UI state to show media. */ init { - viewModelScope.launch(DispatcherProvider.IO) { + viewModelScope.launch { fetchMessage() observeConnectionStateChanges() } @@ -88,7 +87,6 @@ public class MediaGalleryPreviewViewModel( if (result.isSuccess) { this.message = result.data() - headerTitle = message.user.name } } @@ -98,11 +96,13 @@ public class MediaGalleryPreviewViewModel( */ private suspend fun observeConnectionStateChanges() { clientState.connectionState.collect { connectionState -> - // TODO finish when (connectionState) { - ConnectionState.CONNECTED -> onConnected() - ConnectionState.CONNECTING -> {} - ConnectionState.OFFLINE -> {} + ConnectionState.CONNECTED -> { + onConnected() + this.connectionState = connectionState + } + ConnectionState.CONNECTING -> this.connectionState = connectionState + ConnectionState.OFFLINE -> this.connectionState = connectionState } } } From 745b49840181beedf43cee862da305e3aeaf807e Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Fri, 26 Aug 2022 18:21:57 +0200 Subject: [PATCH 19/69] [3369] Start refactoring `MediaGalleryPreviewActivity` to support offline mode --- .../preview/MediaGalleryPreviewActivity.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 6852ae3efed..36b5aa3062b 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -953,7 +953,11 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { private fun defaultMediaOptions(message: Message): List { val user by mediaGalleryPreviewViewModel.user.collectAsState() val saveMediaColor = - if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED) ChatTheme.colors.textHighEmphasis else ChatTheme.colors.disabled + if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED) { + ChatTheme.colors.textHighEmphasis + } else { + ChatTheme.colors.disabled + } val options = mutableListOf( MediaGalleryPreviewOption( @@ -981,7 +985,11 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { if (message.user.id == user?.id) { val deleteColor = - if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED) ChatTheme.colors.errorAccent else ChatTheme.colors.disabled + if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED) { + ChatTheme.colors.errorAccent + } else { + ChatTheme.colors.disabled + } options.add( MediaGalleryPreviewOption( From 9be17c64d5cc217cb0a7ebc5cb71fbdb8140ab39 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 29 Aug 2022 19:33:11 +0200 Subject: [PATCH 20/69] [3369] Make `MediaAttachmentFactory` and its children flexible in the number of previewd thumbnails, including odd numbers. --- .../api/stream-chat-android-compose.api | 8 ++--- .../content/MediaAttachmentContent.kt | 31 ++++++++++--------- .../factory/MediaAttachmentFactory.kt | 26 ++++++++++++---- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index ac95e869cf5..48035e6e2d6 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -594,7 +594,7 @@ public final class io/getstream/chat/android/compose/ui/attachments/content/Link } public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContentKt { - public static final fun MediaAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V + public static final fun MediaAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;ILkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContentKt { @@ -637,10 +637,8 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Comp public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; - public static field lambda-2 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$QuotedAttachmentFactoryKt { @@ -674,8 +672,8 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Link } public final class io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactoryKt { - public static final fun MediaAttachmentFactory (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; - public static synthetic fun MediaAttachmentFactory$default (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; + public static final fun MediaAttachmentFactory (ILkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; + public static synthetic fun MediaAttachmentFactory$default (ILkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; } public final class io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactoryKt { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index bb5924a0216..80cb9220ae5 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -64,6 +64,8 @@ import io.getstream.chat.android.uiutils.extension.hasLink * @param attachmentState The state of the attachment, holding the root modifier, the message * and the onLongItemClick handler. * @param modifier The modifier used for styling. + * @param maximumNumberOfPreviewedItems The maximum number of thumbnails that can be displayed + * in a group when previewing Media attachments in the message list. * @param playButton Represents the play button that is overlaid above video attachment * previews. */ @@ -72,6 +74,7 @@ import io.getstream.chat.android.uiutils.extension.hasLink public fun MediaAttachmentContent( attachmentState: AttachmentState, modifier: Modifier = Modifier, + maximumNumberOfPreviewedItems: Int = 4, playButton: @Composable () -> Unit = { PlayButton() }, ) { val (message, onLongItemClick, _, onMediaGalleryPreviewResult) = attachmentState @@ -110,6 +113,7 @@ public fun MediaAttachmentContent( attachments = attachments, attachmentCount = attachmentCount, gridSpacing = gridSpacing, + maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, message = message, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, @@ -170,6 +174,8 @@ internal fun ShowSingleMediaAttachment( * @param attachments The list of attachments that are to be previewed. * @param attachmentCount The number of attachments that are to be previewed. * @param gridSpacing Determines the spacing strategy between items. + * @param maximumNumberOfPreviewedItems The maximum number of thumbnails that can be displayed + * in a group when previewing Media attachments in the message list. * @param message The original message containing the attachments. * @param onMediaGalleryPreviewResult The result of the activity used for propagating * actions such as media attachment selection, deletion, etc. @@ -183,6 +189,7 @@ internal fun RowScope.ShowMultipleMediaAttachments( attachments: List, attachmentCount: Int, gridSpacing: Dp, + maximumNumberOfPreviewedItems: Int = 4, message: Message, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, onLongItemClick: (Message) -> Unit, @@ -195,7 +202,7 @@ internal fun RowScope.ShowMultipleMediaAttachments( .aspectRatio(TwiceAsTallAsIsWideRatio), verticalArrangement = Arrangement.spacedBy(gridSpacing) ) { - for (attachmentIndex in 0 until MaximumNumberOfItemsInAGrid step 2) { + for (attachmentIndex in 0 until maximumNumberOfPreviewedItems step 2) { if (attachmentIndex < attachmentCount) { MediaAttachmentContentItem( attachment = attachments[attachmentIndex], @@ -216,12 +223,13 @@ internal fun RowScope.ShowMultipleMediaAttachments( .aspectRatio(TwiceAsTallAsIsWideRatio), verticalArrangement = Arrangement.spacedBy(gridSpacing) ) { - for (attachmentIndex in 1..MaximumNumberOfItemsInAGrid step 2) { + for (attachmentIndex in 1 until maximumNumberOfPreviewedItems step 2) { if (attachmentIndex < attachmentCount) { val attachment = attachments[attachmentIndex] val isUploading = attachment.uploadState is Attachment.UploadState.InProgress + val lastItemInColumnIndex = (maximumNumberOfPreviewedItems - 1) - (maximumNumberOfPreviewedItems % 2) - if (attachmentIndex == 3 && attachmentCount > MaximumNumberOfItemsInAGrid) { + if (attachmentIndex == lastItemInColumnIndex && attachmentCount > maximumNumberOfPreviewedItems) { Box(modifier = Modifier.weight(1f)) { MediaAttachmentContentItem( attachment = attachment, @@ -235,7 +243,7 @@ internal fun RowScope.ShowMultipleMediaAttachments( if (!isUploading) { MediaAttachmentViewMoreOverlay( mediaCount = attachmentCount, - mediaIndex = attachmentIndex, + maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, modifier = Modifier.align(Alignment.Center) ) } @@ -331,7 +339,7 @@ internal fun MediaAttachmentContentItem( */ @Composable internal fun PlayButton( - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Box( modifier = modifier, @@ -358,16 +366,17 @@ internal fun PlayButton( * item gallery. * * @param mediaCount The number of total media attachments. - * @param mediaIndex The current media attachment index. + * @param maximumNumberOfPreviewedItems The maximum number of thumbnails that can be displayed + * in a group when previewing Media attachments in the message list. * @param modifier Modifier for styling. */ @Composable internal fun MediaAttachmentViewMoreOverlay( mediaCount: Int, - mediaIndex: Int, + maximumNumberOfPreviewedItems: Int, modifier: Modifier = Modifier, ) { - val remainingMediaCount = mediaCount - (mediaIndex + 1) + val remainingMediaCount = mediaCount - maximumNumberOfPreviewedItems Box( modifier = Modifier @@ -399,9 +408,3 @@ private const val EqualDimensionsRatio = 1f * Composable when calling [Modifier.aspectRatio]. */ private const val TwiceAsTallAsIsWideRatio = 0.5f - -/** - * The maximum numbers of items that can be tiled inside - * [MediaAttachmentContent] - */ -internal const val MaximumNumberOfItemsInAGrid = 4 diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt index 49d83fcb865..cfd8fc3a230 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory -import io.getstream.chat.android.compose.ui.attachments.content.MaximumNumberOfItemsInAGrid import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentContent import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentPreviewContent import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentPreviewItemSize @@ -39,6 +38,8 @@ import io.getstream.chat.android.uiutils.constant.AttachmentType /** * An [AttachmentFactory] that is able to handle Image and Video attachments. * + * @param maximumNumberOfPreviewedItems The maximum number of thumbnails that can be displayed + * in a group when previewing Media attachments in the message list. * @param contentPlayButton Displays a play button above video attachments * in the messages list. * @param previewContentPlayButton Displays a play button above video attachments @@ -46,8 +47,15 @@ import io.getstream.chat.android.uiutils.constant.AttachmentType */ @Suppress("FunctionName") public fun MediaAttachmentFactory( - contentPlayButton: @Composable () -> Unit = { DefaultContentPlayButton() }, - previewContentPlayButton: @Composable () -> Unit = { DefaultPreviewContentPlayButton() }, + maximumNumberOfPreviewedItems: Int = 4, + contentPlayButton: @Composable () -> Unit = { + DefaultContentPlayButton( + maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems + ) + }, + previewContentPlayButton: @Composable () -> Unit = { + DefaultPreviewContentPlayButton() + }, ): AttachmentFactory = AttachmentFactory( canHandle = { @@ -70,6 +78,7 @@ public fun MediaAttachmentFactory( .wrapContentHeight() .heightIn(max = ChatTheme.dimens.attachmentsContentImageMaxHeight), attachmentState = state, + maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, playButton = { contentPlayButton() } ) } @@ -79,16 +88,21 @@ public fun MediaAttachmentFactory( * Represents the default play button that is * overlaid above video attachment previews inside * the messages list. + * + * @param maximumNumberOfPreviewedItems The maximum number of thumbnails that can be displayed + * in a group when previewing Media attachments in the message list. */ @Composable -private fun DefaultContentPlayButton() { +private fun DefaultContentPlayButton( + maximumNumberOfPreviewedItems: Int, +) { PlayButton( modifier = Modifier .shadow(10.dp, shape = CircleShape) .background(color = Color.White, shape = CircleShape) .size( - width = ChatTheme.dimens.attachmentsContentImageWidth / MaximumNumberOfItemsInAGrid, - height = ChatTheme.dimens.attachmentsContentImageWidth / MaximumNumberOfItemsInAGrid, + width = ChatTheme.dimens.attachmentsContentImageWidth / maximumNumberOfPreviewedItems, + height = ChatTheme.dimens.attachmentsContentImageWidth / maximumNumberOfPreviewedItems, ) ) } From 3096464c0db55704c866d49d0b1cdde03baaff3c Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 30 Aug 2022 18:02:06 +0200 Subject: [PATCH 21/69] [3369] Separate image and video maximum height in `ChatDimens` add name specific grid spacing for media preview --- .../api/stream-chat-android-compose.api | 14 +++++++---- .../content/MediaAttachmentContent.kt | 12 ++++++++-- .../factory/MediaAttachmentFactory.kt | 7 ++---- .../android/compose/ui/theme/StreamDimens.kt | 24 ++++++++++++++++++- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 48035e6e2d6..f9074598532 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1628,8 +1628,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors$Compa public final class io/getstream/chat/android/compose/ui/theme/StreamDimens { public static final field Companion Lio/getstream/chat/android/compose/ui/theme/StreamDimens$Companion; - public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-D9Ej5fM ()F public final fun component10-D9Ej5fM ()F public final fun component11-D9Ej5fM ()F @@ -1669,13 +1669,16 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamDimens { public final fun component42-D9Ej5fM ()F public final fun component43-D9Ej5fM ()F public final fun component44-D9Ej5fM ()F + public final fun component45-D9Ej5fM ()F + public final fun component46-D9Ej5fM ()F + public final fun component47-D9Ej5fM ()F public final fun component5-D9Ej5fM ()F public final fun component6-D9Ej5fM ()F public final fun component7-D9Ej5fM ()F public final fun component8-D9Ej5fM ()F public final fun component9-D9Ej5fM ()F - public final fun copy-sxsZ-nI (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; - public static synthetic fun copy-sxsZ-nI$default (Lio/getstream/chat/android/compose/ui/theme/StreamDimens;FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; + public final fun copy-TVlVuw8 (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; + public static synthetic fun copy-TVlVuw8$default (Lio/getstream/chat/android/compose/ui/theme/StreamDimens;FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; public fun equals (Ljava/lang/Object;)Z public final fun getAttachmentsContentFileUploadWidth-D9Ej5fM ()F public final fun getAttachmentsContentFileWidth-D9Ej5fM ()F @@ -1686,6 +1689,9 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamDimens { public final fun getAttachmentsContentImageMaxHeight-D9Ej5fM ()F public final fun getAttachmentsContentImageWidth-D9Ej5fM ()F public final fun getAttachmentsContentLinkWidth-D9Ej5fM ()F + public final fun getAttachmentsContentMediaGridSpacing-D9Ej5fM ()F + public final fun getAttachmentsContentMediaWidth-D9Ej5fM ()F + public final fun getAttachmentsContentVideoMaxHeight-D9Ej5fM ()F public final fun getChannelAvatarSize-D9Ej5fM ()F public final fun getChannelItemHorizontalPadding-D9Ej5fM ()F public final fun getChannelItemVerticalPadding-D9Ej5fM ()F diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 80cb9220ae5..ac83079a9b9 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.Text import androidx.compose.material.ripple.rememberRipple @@ -78,8 +79,7 @@ public fun MediaAttachmentContent( playButton: @Composable () -> Unit = { PlayButton() }, ) { val (message, onLongItemClick, _, onMediaGalleryPreviewResult) = attachmentState - // TODO add media grid spacing to chat theme - val gridSpacing = ChatTheme.dimens.attachmentsContentImageGridSpacing + val gridSpacing = ChatTheme.dimens.attachmentsContentMediaGridSpacing Row( modifier @@ -155,9 +155,17 @@ internal fun ShowSingleMediaAttachment( } } } + MediaAttachmentContentItem( attachment = attachment, modifier = Modifier + .heightIn( + max = if (attachment.type == AttachmentType.VIDEO) { + ChatTheme.dimens.attachmentsContentVideoMaxHeight + } else { + ChatTheme.dimens.attachmentsContentImageMaxHeight + } + ) .fillMaxWidth() .aspectRatio(ratio ?: EqualDimensionsRatio), message = message, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt index cfd8fc3a230..55f797dbba4 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt @@ -17,10 +17,9 @@ package io.getstream.chat.android.compose.ui.attachments.factory import androidx.compose.foundation.background -import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -74,9 +73,7 @@ public fun MediaAttachmentFactory( content = @Composable { modifier, state -> MediaAttachmentContent( modifier = modifier - .width(ChatTheme.dimens.attachmentsContentImageWidth) - .wrapContentHeight() - .heightIn(max = ChatTheme.dimens.attachmentsContentImageMaxHeight), + .width(ChatTheme.dimens.attachmentsContentMediaWidth), attachmentState = state, maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, playButton = { contentPlayButton() } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt index 5a16f9675f6..31b199973ee 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt @@ -67,7 +67,11 @@ import androidx.compose.ui.unit.dp * @param groupAvatarInitialsXOffset The x offset of the user initials inside avatar when there are more than two users. * @param groupAvatarInitialsYOffset The y offset of the user initials inside avatar when there are more than two users. * @param attachmentsContentImageMaxHeight The maximum height an image attachment will expand to while automatically - * re-sizing itself in order to obey its aspect ratio. + * re-sizing itself in order to obey its aspect ratio. + * @param attachmentsContentVideoMaxHeight The maximum height video attachment will expand to while automatically + * re-sizing itself in order to obey its aspect ratio. + * @param attachmentsContentMediaGridSpacing The spacing between media preview tiles in the message list. + * @param attachmentsContentMediaWidth The width of media attachment previews in the message list. */ @Immutable public data class StreamDimens @@ -84,7 +88,9 @@ constructor( public val selectedChannelMenuUserItemWidth: Dp, public val selectedChannelMenuUserItemHorizontalPadding: Dp, public val selectedChannelMenuUserItemAvatarSize: Dp, + // TODO - deprecate in favor of attachmentsContentMediaWidth public val attachmentsContentImageWidth: Dp, + // TODO - deprecate in favor of attachmentsContentMediaGridSpacing public val attachmentsContentImageGridSpacing: Dp, public val attachmentsContentImageHeight: Dp, public val attachmentsContentGiphyWidth: Dp, @@ -122,6 +128,9 @@ constructor( public val groupAvatarInitialsXOffset: Dp, public val groupAvatarInitialsYOffset: Dp, public val attachmentsContentImageMaxHeight: Dp, + public val attachmentsContentVideoMaxHeight: Dp, + public val attachmentsContentMediaGridSpacing: Dp, + public val attachmentsContentMediaWidth: Dp, ) { /** @@ -175,6 +184,10 @@ constructor( * users. * @param attachmentsContentImageMaxHeight The maximum height an image attachment will expand to while automatically * re-sizing itself in order to obey its aspect ratio. + * @param attachmentsContentVideoMaxHeight The maximum height video attachment will expand to while automatically + * re-sizing itself in order to obey its aspect ratio. + * @param attachmentsContentMediaGridSpacing The spacing between media preview tiles in the message list. + * @param attachmentsContentMediaWidth The width of media attachment previews in the message list. */ public constructor( channelItemVerticalPadding: Dp, @@ -220,6 +233,9 @@ constructor( groupAvatarInitialsXOffset: Dp, groupAvatarInitialsYOffset: Dp, attachmentsContentImageMaxHeight: Dp, + attachmentsContentVideoMaxHeight: Dp, + attachmentsContentMediaGridSpacing: Dp, + attachmentsContentMediaWidth: Dp, ) : this( channelItemVerticalPadding = channelItemVerticalPadding, channelItemHorizontalPadding = channelItemHorizontalPadding, @@ -265,6 +281,9 @@ constructor( groupAvatarInitialsXOffset = groupAvatarInitialsXOffset, groupAvatarInitialsYOffset = groupAvatarInitialsYOffset, attachmentsContentImageMaxHeight = attachmentsContentImageMaxHeight, + attachmentsContentVideoMaxHeight = attachmentsContentVideoMaxHeight, + attachmentsContentMediaGridSpacing = attachmentsContentMediaGridSpacing, + attachmentsContentMediaWidth = attachmentsContentMediaWidth ) public companion object { @@ -318,6 +337,9 @@ constructor( groupAvatarInitialsXOffset = 1.5.dp, groupAvatarInitialsYOffset = 2.5.dp, attachmentsContentImageMaxHeight = 600.dp, + attachmentsContentVideoMaxHeight = 400.dp, + attachmentsContentMediaGridSpacing = 2.dp, + attachmentsContentMediaWidth = 250.dp ) } } From 923407e26a08e76da803ecf1826d7f2ea962e47a Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Wed, 31 Aug 2022 23:20:26 +0200 Subject: [PATCH 22/69] [3369] Improve the offline state for the new media gallery preview screen and implement retry procedure when reconnecting after having a failed load due to being offline. --- .../api/stream-chat-android-compose.api | 27 +- ...iaGalleryPreviewActivityAttachmentState.kt | 79 ++++++ .../MediaGalleryPreviewActivityState.kt | 73 +++++ .../content/MediaAttachmentContent.kt | 2 +- .../preview/MediaGalleryPreviewActivity.kt | 260 ++++++++++++------ .../preview/MediaGalleryPreviewContract.kt | 7 +- .../android/compose/ui/util/ImageUtils.kt | 42 +++ .../MediaGalleryPreviewViewModel.kt | 23 +- 8 files changed, 416 insertions(+), 97 deletions(-) create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState.kt create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityState.kt diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index f9074598532..d8ae09796ac 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -190,6 +190,22 @@ public final class io/getstream/chat/android/compose/state/imagepreview/ImagePre public static fun values ()[Lio/getstream/chat/android/compose/state/imagepreview/ImagePreviewResultType; } +public final class io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityState$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityState; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityState; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult : android/os/Parcelable { public static final field $stable I public static final field CREATOR Landroid/os/Parcelable$Creator; @@ -699,11 +715,9 @@ public final class io/getstream/chat/android/compose/ui/attachments/preview/Comp public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/preview/ComposableSingletons$MediaGalleryPreviewActivityKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; public static field lambda-2 Lkotlin/jvm/functions/Function2; - public static field lambda-3 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-3$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/chat/android/compose/ui/attachments/preview/ComposableSingletons$MediaPreviewActivityKt { @@ -749,7 +763,7 @@ public final class io/getstream/chat/android/compose/ui/attachments/preview/Medi } public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity$Companion { - public final fun getIntent (Landroid/content/Context;Ljava/lang/String;I)Landroid/content/Intent; + public final fun getIntent (Landroid/content/Context;Lio/getstream/chat/android/client/models/Message;I)Landroid/content/Intent; } public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract : androidx/activity/result/contract/ActivityResultContract { @@ -763,10 +777,10 @@ public final class io/getstream/chat/android/compose/ui/attachments/preview/Medi public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract$Input { public static final field $stable I - public fun (Ljava/lang/String;I)V - public synthetic fun (Ljava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/client/models/Message;I)V + public synthetic fun (Lio/getstream/chat/android/client/models/Message;IILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getInitialPosition ()I - public final fun getMessageId ()Ljava/lang/String; + public final fun getMessage ()Lio/getstream/chat/android/client/models/Message; } public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity : androidx/appcompat/app/AppCompatActivity { @@ -1837,6 +1851,7 @@ public final class io/getstream/chat/android/compose/ui/util/ChannelUtilsKt { public final class io/getstream/chat/android/compose/ui/util/ImageUtilsKt { public static final fun mirrorRtl (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/unit/LayoutDirection;)Landroidx/compose/ui/Modifier; + public static final fun rememberStreamImagePainter-MqR-F_0 (Lcoil/request/ImageRequest;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/layout/ContentScale;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter; public static final fun rememberStreamImagePainter-MqR-F_0 (Ljava/lang/Object;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/layout/ContentScale;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter; } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState.kt new file mode 100644 index 00000000000..4408cb7cc64 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.state.mediagallerypreview + +import android.os.Parcelable +import io.getstream.chat.android.client.models.Attachment +import kotlinx.parcelize.Parcelize + +/** + * Class used to transform [Attachment] into a smaller and easily + * parcelable version that contains the minimum necessary data + * for the proper functioning of the Media Gallery Preview screen. + * + * @param name The name of the attachment. + * @param url The url of the file. + * @param thumbUrl The URL for the thumbnail version of the attachment, + * given the attachment has a visual quality, e.g. is a video, an image, + * a link to a website or similar. + * @param imageUrl The URL for the raw version of the attachment. + * @param assetUrl The URL for the asset. + * @param originalHeight The original height of the attachment. + * Provided if the attachment is of type "image". + * @param originalWidth The original width of the attachment. + * Provided if the attachment is of type "image". + */ +@Parcelize +internal class MediaGalleryPreviewActivityAttachmentState( + val name: String?, + val url: String?, + val thumbUrl: String?, + val imageUrl: String?, + val assetUrl: String?, + val originalWidth: Int?, + val originalHeight: Int?, + val type: String?, +) : Parcelable + +/** + * Maps [Attachment] to [MediaGalleryPreviewActivityAttachmentState]. + */ +internal fun Attachment.toMediaGalleryPreviewActivityAttachmentState(): MediaGalleryPreviewActivityAttachmentState = + MediaGalleryPreviewActivityAttachmentState( + name = this.name, + url = this.url, + thumbUrl = this.thumbUrl, + imageUrl = this.imageUrl, + assetUrl = this.assetUrl, + originalWidth = this.originalWidth, + originalHeight = this.originalHeight, + type = this.type + ) + +/** + * Maps [MediaGalleryPreviewActivityAttachmentState] to [Attachment]. + */ +internal fun MediaGalleryPreviewActivityAttachmentState.toAttachment(): Attachment = Attachment( + name = this.name, + url = this.url, + thumbUrl = this.thumbUrl, + imageUrl = this.imageUrl, + assetUrl = this.assetUrl, + originalWidth = this.originalWidth, + originalHeight = this.originalHeight, + type = this.type +) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityState.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityState.kt new file mode 100644 index 00000000000..d16c7a55f37 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityState.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.state.mediagallerypreview + +import android.os.Parcelable +import io.getstream.chat.android.client.models.Message +import io.getstream.chat.android.client.models.User +import kotlinx.parcelize.Parcelize + +/** + * Class used to parcelize the minimum necessary information + * for proper function of the Media Gallery Preview screen. + * + * Using it avoids having to parcelize client models and parcelizing + * overly large models. + * + * @param messageId The ID of the message containing the attachments. + * @param userId The ID of the user who sent the message. + * @param userName The name of the user who sent the message. + * @param userImage The image of the user who sent the message. + * @param userIsOnline The online status of the user who sent the message. + * Set to false because we don't track the status inside the preview screen. + * @param attachments The list of attachments contained in the original message. + */ +@Parcelize +internal data class MediaGalleryPreviewActivityState( + val messageId: String, + val userId: String, + val userName: String, + val userImage: String, + val userIsOnline: Boolean = false, + val attachments: List, +) : Parcelable + +/** + * Maps [Message] to [toMediaGalleryPreviewActivityState]. + */ +internal fun Message.toMediaGalleryPreviewActivityState(): MediaGalleryPreviewActivityState = + MediaGalleryPreviewActivityState( + messageId = this.id, + userId = this.user.id, + userName = this.user.name, + userImage = this.user.image, + attachments = this.attachments.map { it.toMediaGalleryPreviewActivityAttachmentState() } + ) + +/** + * Maps [toMediaGalleryPreviewActivityState] to [Message]. + */ +internal fun MediaGalleryPreviewActivityState.toMessage(): Message = + Message( + id = this.messageId, + user = User( + id = this.userId, + name = this.userName, + image = this.userImage + ), + attachments = this.attachments.map { it.toAttachment() }.toMutableList() + ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index ac83079a9b9..81a2ea61215 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -316,7 +316,7 @@ internal fun MediaAttachmentContentItem( onClick = { mixedMediaPreviewLauncher.launch( MediaGalleryPreviewContract.Input( - messageId = message.id, + message = message, initialPosition = attachmentPosition ) ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 36b5aa3062b..e1d1e3e29e3 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -79,6 +79,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Scaffold @@ -113,6 +114,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import coil.compose.AsyncImagePainter +import coil.request.ImageRequest import com.getstream.sdk.chat.StreamFileUtil import com.getstream.sdk.chat.images.StreamImageLoader import com.getstream.sdk.chat.utils.extensions.imagePreviewUrl @@ -130,12 +132,15 @@ import io.getstream.chat.android.compose.handlers.DownloadPermissionHandler import io.getstream.chat.android.compose.handlers.PermissionHandler import io.getstream.chat.android.compose.state.mediagallerypreview.Delete import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewAction +import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewActivityState import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewOption import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResultType import io.getstream.chat.android.compose.state.mediagallerypreview.Reply import io.getstream.chat.android.compose.state.mediagallerypreview.SaveMedia import io.getstream.chat.android.compose.state.mediagallerypreview.ShowInChat +import io.getstream.chat.android.compose.state.mediagallerypreview.toMediaGalleryPreviewActivityState +import io.getstream.chat.android.compose.state.mediagallerypreview.toMessage import io.getstream.chat.android.compose.ui.attachments.content.PlayButton import io.getstream.chat.android.compose.ui.components.LoadingIndicator import io.getstream.chat.android.compose.ui.components.NetworkLoadingIndicator @@ -163,7 +168,9 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { private val factory by lazy { MediaGalleryPreviewViewModelFactory( chatClient = ChatClient.instance(), - messageId = intent?.getStringExtra(KeyMessageId) ?: "" + messageId = intent?.getParcelableExtra( + KeyMediaGalleryPreviewActivityState + )?.messageId ?: "" ) } @@ -179,7 +186,18 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val messageId = intent?.getStringExtra(KeyMessageId) ?: "" + val mediaGalleryPreviewActivityState = intent?.getParcelableExtra( + KeyMediaGalleryPreviewActivityState + ) + val messageId = mediaGalleryPreviewActivityState?.messageId ?: "" + + if (!mediaGalleryPreviewViewModel.hasCompleteMessage) { + val message = mediaGalleryPreviewActivityState?.toMessage() + + if (message != null) + mediaGalleryPreviewViewModel.message = message + } + val attachmentPosition = intent?.getIntExtra(KeyAttachmentPosition, 0) ?: 0 if (messageId.isBlank()) { @@ -596,7 +614,21 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - val painter = rememberStreamImagePainter(data = attachment.imagePreviewUrl) + + // Used as a workaround for Coil's lack of a retry policy. + // See: https://github.com/coil-kt/coil/issues/884#issuecomment-975932886 + var retryHash by remember { + mutableStateOf(0) + } + + val painter = + rememberStreamImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(attachment.imagePreviewUrl) + .crossfade(true) + .setParameter(key = "retry_hash", value = retryHash) + .build() + ) val density = LocalDensity.current val parentSize = Size(density.run { maxWidth.toPx() }, density.run { maxHeight.toPx() }) @@ -607,93 +639,156 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { val scale by animateFloatAsState(targetValue = currentScale) + // Used to refresh the request for the current page + // if it has previously failed. + if (page == pagerState.currentPage && + mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && + painter.state is AsyncImagePainter.State.Error + ) { + retryHash++ + } + val transformModifier = if (painter.state is AsyncImagePainter.State.Success) { val size = painter.intrinsicSize - Modifier.aspectRatio(size.width / size.height, true) + Modifier + .aspectRatio(size.width / size.height, true) + .background(color = ChatTheme.colors.overlay) } else { Modifier } - Image( - modifier = transformModifier - .graphicsLayer( - scaleY = scale, - scaleX = scale, - translationX = translation.x, - translationY = translation.y - ) - .onGloballyPositioned { - imageSize = Size(it.size.width.toFloat(), it.size.height.toFloat()) - } - .pointerInput(Unit) { - forEachGesture { - awaitPointerEventScope { - awaitFirstDown(requireUnconsumed = true) - do { - val event = awaitPointerEvent(pass = PointerEventPass.Initial) - - val zoom = event.calculateZoom() - currentScale = (zoom * currentScale).coerceAtMost(MaxZoomScale) - - val maxTranslation = calculateMaxOffset( - imageSize = imageSize, - scale = currentScale, - parentSize = parentSize - ) - - val offset = event.calculatePan() - val newTranslationX = translation.x + offset.x * currentScale - val newTranslationY = translation.y + offset.y * currentScale - - translation = Offset( - newTranslationX.coerceIn(-maxTranslation.x, maxTranslation.x), - newTranslationY.coerceIn(-maxTranslation.y, maxTranslation.y) - ) - - if (abs(newTranslationX) < calculateMaxOffsetPerAxis( - imageSize.width, - currentScale, - parentSize.width - ) || zoom != DefaultZoomScale - ) { - event.changes.forEach { it.consume() } + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + ImagePlaceHolder(asyncImagePainterState = painter.state) + + Image( + modifier = transformModifier + .graphicsLayer( + scaleY = scale, + scaleX = scale, + translationX = translation.x, + translationY = translation.y + ) + .onGloballyPositioned { + imageSize = Size(it.size.width.toFloat(), it.size.height.toFloat()) + } + .pointerInput(Unit) { + forEachGesture { + awaitPointerEventScope { + awaitFirstDown(requireUnconsumed = true) + do { + val event = awaitPointerEvent(pass = PointerEventPass.Initial) + + val zoom = event.calculateZoom() + currentScale = (zoom * currentScale).coerceAtMost(MaxZoomScale) + + val maxTranslation = calculateMaxOffset( + imageSize = imageSize, + scale = currentScale, + parentSize = parentSize + ) + + val offset = event.calculatePan() + val newTranslationX = translation.x + offset.x * currentScale + val newTranslationY = translation.y + offset.y * currentScale + + translation = Offset( + newTranslationX.coerceIn(-maxTranslation.x, maxTranslation.x), + newTranslationY.coerceIn(-maxTranslation.y, maxTranslation.y) + ) + + if (abs(newTranslationX) < calculateMaxOffsetPerAxis( + imageSize.width, + currentScale, + parentSize.width + ) || zoom != DefaultZoomScale + ) { + event.changes.forEach { it.consume() } + } + } while (event.changes.any { it.pressed }) + + if (currentScale < DefaultZoomScale) { + currentScale = DefaultZoomScale } - } while (event.changes.any { it.pressed }) - - if (currentScale < DefaultZoomScale) { - currentScale = DefaultZoomScale } } } - } - .pointerInput(Unit) { - forEachGesture { - awaitPointerEventScope { - awaitFirstDown() - withTimeoutOrNull(DoubleTapTimeoutMs) { + .pointerInput(Unit) { + forEachGesture { + awaitPointerEventScope { awaitFirstDown() - currentScale = when { - currentScale == MaxZoomScale -> DefaultZoomScale - currentScale >= MidZoomScale -> MaxZoomScale - else -> MidZoomScale - } - - if (currentScale == DefaultZoomScale) { - translation = Offset(0f, 0f) + withTimeoutOrNull(DoubleTapTimeoutMs) { + awaitFirstDown() + currentScale = when { + currentScale == MaxZoomScale -> DefaultZoomScale + currentScale >= MidZoomScale -> MaxZoomScale + else -> MidZoomScale + } + + if (currentScale == DefaultZoomScale) { + translation = Offset(0f, 0f) + } } } } - } - }, + }, + painter = painter, + contentDescription = null + ) + + Log.d("isCurrentPage", "${page != pagerState.currentPage}") + + if (pagerState.currentPage != page) { + currentScale = DefaultZoomScale + translation = Offset(0f, 0f) + } + } + } + } + + /** + * Displays an image icon if no image was loaded previously + * or the request has failed, a circular progress indicator + * if the image is loading or nothing if the image has successfully + * loaded. + * + * @param asyncImagePainterState The painter state used to determine + * which UI to show. + */ + @Composable + private fun ImagePlaceHolder(asyncImagePainterState: AsyncImagePainter.State) { + val painter = painterResource( + id = R.drawable.stream_compose_ic_image_picker + ) + + val imageModifier = Modifier.fillMaxSize(0.4f) + + when (asyncImagePainterState) { + is AsyncImagePainter.State.Loading -> { + CircularProgressIndicator( + modifier = Modifier + .padding(horizontal = 8.dp) + .size(50.dp), + strokeWidth = 5.dp, + color = ChatTheme.colors.primaryAccent + ) + } + is AsyncImagePainter.State.Error -> Icon( + tint = ChatTheme.colors.textLowEmphasis, + modifier = imageModifier, painter = painter, contentDescription = null ) - - Log.d("isCurrentPage", "${page != pagerState.currentPage}") - - if (pagerState.currentPage != page) { - currentScale = DefaultZoomScale - translation = Offset(0f, 0f) + is AsyncImagePainter.State.Success -> {} + is AsyncImagePainter.State.Empty -> { + Icon( + tint = ChatTheme.colors.textLowEmphasis, + modifier = imageModifier, + painter = painter, + contentDescription = null + ) } } } @@ -908,12 +1003,17 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { ) { IconButton( modifier = Modifier.align(Alignment.CenterStart), - onClick = { onShareMediaClick(attachments[pagerState.currentPage]) } + onClick = { onShareMediaClick(attachments[pagerState.currentPage]) }, + enabled = mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED ) { Icon( painter = painterResource(id = R.drawable.stream_compose_ic_share), contentDescription = stringResource(id = R.string.stream_compose_image_preview_share), - tint = ChatTheme.colors.textHighEmphasis, + tint = if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED) { + ChatTheme.colors.textHighEmphasis + } else { + ChatTheme.colors.disabled + }, ) } @@ -1207,7 +1307,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { /** * Represents the key for the ID of the message with the attachments we're browsing. */ - private const val KeyMessageId: String = "messageId" + private const val KeyMediaGalleryPreviewActivityState: String = "mediaGalleryPreviewActivityState" /** * Represents the key for the starting attachment position based on the clicked attachment. @@ -1243,12 +1343,14 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { * Used to build an [Intent] to start the [MediaGalleryPreviewActivity] with the required data. * * @param context The context to start the activity with. - * @param messageId The ID of the message to explore the media of. + * @param message The [Message] containing the attachments. * @param attachmentPosition The initial position of the clicked media attachment. */ - public fun getIntent(context: Context, messageId: String, attachmentPosition: Int): Intent { + public fun getIntent(context: Context, message: Message, attachmentPosition: Int): Intent { return Intent(context, MediaGalleryPreviewActivity::class.java).apply { - putExtra(KeyMessageId, messageId) + val mediaGalleryPreviewActivityState = message.toMediaGalleryPreviewActivityState() + + putExtra(KeyMediaGalleryPreviewActivityState, mediaGalleryPreviewActivityState) putExtra(KeyAttachmentPosition, attachmentPosition) } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt index 457dc10e69f..3ab6b7eca65 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt @@ -19,6 +19,7 @@ package io.getstream.chat.android.compose.ui.attachments.preview import android.content.Context import android.content.Intent import androidx.activity.result.contract.ActivityResultContract +import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult /** @@ -36,7 +37,7 @@ public class MediaGalleryPreviewContract : override fun createIntent(context: Context, input: Input): Intent { return MediaGalleryPreviewActivity.getIntent( context, - messageId = input.messageId, + message = input.message, attachmentPosition = input.initialPosition ) } @@ -53,11 +54,11 @@ public class MediaGalleryPreviewContract : /** * Defines the input for the [MediaGalleryPreviewContract]. * - * @param messageId The ID of the message. + * @param message The message containing the attachments. * @param initialPosition The initial position of the Image gallery, based on the clicked item. */ public class Input( - public val messageId: String, + public val message: Message, public val initialPosition: Int = 0, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt index ec71eecaa32..9abca63490d 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt @@ -115,3 +115,45 @@ public fun rememberStreamImagePainter( filterQuality = filterQuality ) } + +/** + * Wrapper around the [coil.compose.rememberAsyncImagePainter] that plugs in our [LocalStreamImageLoader] singleton + * that can be used to customize all image loading requests, like adding headers, interceptors and similar. + * + * @param model The [ImageRequest] used to load the given image. + * @param placeholderPainter The painter used as a placeholder, while loading. + * @param errorPainter The painter used when the request fails. + * @param fallbackPainter The painter used as a fallback, in case the data is null. + * @param onLoading Handler when the loading starts. + * @param onSuccess Handler when the request is successful. + * @param onError Handler when the request fails. + * @param contentScale The scaling model to use for the image. + * @param filterQuality The quality algorithm used when scaling the image. + * + * @return The [AsyncImagePainter] that remembers the request and the image that we want to show. + */ +@Composable +public fun rememberStreamImagePainter( + model: ImageRequest, + placeholderPainter: Painter? = null, + errorPainter: Painter? = null, + fallbackPainter: Painter? = errorPainter, + onLoading: ((AsyncImagePainter.State.Loading) -> Unit)? = null, + onSuccess: ((AsyncImagePainter.State.Success) -> Unit)? = null, + onError: ((AsyncImagePainter.State.Error) -> Unit)? = null, + contentScale: ContentScale = ContentScale.Fit, + filterQuality: FilterQuality = DrawScope.DefaultFilterQuality, +): AsyncImagePainter { + return rememberAsyncImagePainter( + model = model, + imageLoader = LocalStreamImageLoader.current, + placeholder = placeholderPainter, + error = errorPainter, + fallback = fallbackPainter, + contentScale = contentScale, + onSuccess = onSuccess, + onError = onError, + onLoading = onLoading, + filterQuality = filterQuality, + ) +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt index 673d22b29e1..a7054ddcbc8 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt @@ -27,7 +27,6 @@ import io.getstream.chat.android.client.models.ConnectionState import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.client.models.User import io.getstream.chat.android.client.setup.state.ClientState -import io.getstream.chat.android.uiutils.constant.AttachmentType import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -45,11 +44,22 @@ public class MediaGalleryPreviewViewModel( */ public val user: StateFlow = chatClient.clientState.user + /** + * Indicates if we have fetched the complete message from the backend. + * + * This is necessary because our first state is set via a minimum viable + * data set needed to display the full UI in offline state. + * + * @see [io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewActivityAttachmentState] + * and [io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewActivity.getIntent] + */ + internal var hasCompleteMessage: Boolean = false + /** * Represents the message that we observe to show the UI data. */ public var message: Message by mutableStateOf(Message()) - private set + internal set /** * Represent the header title of the gallery screen. @@ -87,6 +97,7 @@ public class MediaGalleryPreviewViewModel( if (result.isSuccess) { this.message = result.data() + hasCompleteMessage = true } } @@ -108,7 +119,7 @@ public class MediaGalleryPreviewViewModel( } private suspend fun onConnected() { - if (message.id.isEmpty()) { + if (message.id.isEmpty() || !hasCompleteMessage) { fetchMessage() } } @@ -147,11 +158,7 @@ public class MediaGalleryPreviewViewModel( val message = message attachments.removeAll { - it.assetUrl == currentMediaAttachment.assetUrl || - ( - currentMediaAttachment.type == AttachmentType.IMAGE && - it.imageUrl == currentMediaAttachment.imageUrl - ) + it.url == currentMediaAttachment.url } chatClient.updateMessage(message).enqueue() From 4af957e2e4258c8f050bf1ff0b6c098be740f94b Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Wed, 31 Aug 2022 23:22:53 +0200 Subject: [PATCH 23/69] [3369] Generate detekt baseline --- stream-chat-android-compose/detekt-baseline.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index be23e51350d..7bdc1cc1027 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -5,6 +5,7 @@ ComplexCondition:MessageItem.kt$!messageItem.isMine && ( messageItem.shouldShowFooter || messageItem.groupPosition == Bottom || messageItem.groupPosition == None ) ComplexCondition:MessageOptions.kt$((isOwnMessage && canEditOwnMessage) || canEditAnyMessage) && !selectedMessage.isGiphy() ComplexCondition:Messages.kt$!endOfMessages && index == messages.lastIndex && messages.isNotEmpty() && lazyListState.isScrollInProgress + ComplexMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun ImagePreviewContent( attachment: Attachment, pagerState: PagerState, page: Int, ) ComplexMethod:MessageListViewModel.kt$MessageListViewModel$private fun groupMessages( messages: List<Message>, isInThread: Boolean, reads: List<ChannelUserRead>, ): List<MessageListItemState> ComplexMethod:MessageOptions.kt$@Composable public fun defaultMessageOptionsState( selectedMessage: Message, currentUser: User?, isInThread: Boolean, ownCapabilities: Set<String>, ): List<MessageOptionItemState> ForbiddenComment:MessageText.kt$// TODO: Fix emoji font padding once this is resolved and exposed: https://issuetracker.google.com/issues/171394808 @@ -19,6 +20,7 @@ LongMethod:MessageOptions.kt$@Composable public fun defaultMessageOptionsState( selectedMessage: Message, currentUser: User?, isInThread: Boolean, ownCapabilities: Set<String>, ): List<MessageOptionItemState> LongMethod:Messages.kt$@Composable public fun Messages( messagesState: MessagesState, lazyListState: LazyListState, onMessagesStartReached: () -> Unit, onLastVisibleMessageChanged: (Message) -> Unit, onScrolledToBottom: () -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(vertical = 16.dp), helperContent: @Composable BoxScope.() -> Unit = { DefaultMessagesHelperContent(messagesState, lazyListState) }, loadingMoreContent: @Composable () -> Unit = { DefaultMessagesLoadingMoreIndicator() }, itemContent: @Composable (MessageListItemState) -> Unit, ) LongMethod:StreamTypography.kt$StreamTypography.Companion$public fun defaultTypography(fontFamily: FontFamily? = null): StreamTypography + LongParameterList:MediaGalleryPreviewActivityAttachmentState.kt$MediaGalleryPreviewActivityAttachmentState$( val name: String?, val url: String?, val thumbUrl: String?, val imageUrl: String?, val assetUrl: String?, val originalWidth: Int?, val originalHeight: Int?, val type: String?, ) LongParameterList:MessageComposer.kt$( value: String, coolDownTime: Int, attachments: List<Attachment>, validationErrors: List<ValidationError>, ownCapabilities: Set<String>, onSendMessage: (String, List<Attachment>) -> Unit, ) LongParameterList:Messages.kt$( messagesState: MessagesState, lazyListState: LazyListState, onMessagesStartReached: () -> Unit, onLastVisibleMessageChanged: (Message) -> Unit, onScrolledToBottom: () -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(vertical = 16.dp), helperContent: @Composable BoxScope.() -> Unit = { DefaultMessagesHelperContent(messagesState, lazyListState) }, loadingMoreContent: @Composable () -> Unit = { DefaultMessagesLoadingMoreIndicator() }, itemContent: @Composable (MessageListItemState) -> Unit, ) LongParameterList:MessagesScreen.kt$( context: Context, channelId: String, enforceUniqueReactions: Boolean, messageLimit: Int, showDateSeparators: Boolean, showSystemMessages: Boolean, deletedMessageVisibility: DeletedMessageVisibility, messageFooterVisibility: MessageFooterVisibility, ) @@ -29,9 +31,9 @@ MagicNumber:ImageAttachmentContent.kt$4 MagicNumber:ImagePreviewActivity.kt$ImagePreviewActivity$8f MagicNumber:MediaAttachmentContent.kt$0.85f - MagicNumber:MediaAttachmentContent.kt$3 MagicNumber:MediaAttachmentContent.kt$8 MagicNumber:MediaAttachmentFactory.kt$4 + MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$0.4f MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$6 MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$8f MagicNumber:Messages.kt$3 From 3dee35a8b3b6593663361bf1077c2148e0c898bc Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 1 Sep 2022 18:19:15 +0200 Subject: [PATCH 24/69] [3369] Improve the design and offline capabilities of `MediaGalleryPreviewOptions` --- .../api/stream-chat-android-compose.api | 10 ++- .../preview/MediaGalleryPreviewActivity.kt | 85 +++++++++++++++---- .../android/compose/ui/theme/StreamColors.kt | 10 +++ 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index d8ae09796ac..e891e882909 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1587,7 +1587,7 @@ public final class io/getstream/chat/android/compose/ui/theme/ChatThemeKt { public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public static final field Companion Lio/getstream/chat/android/compose/ui/theme/StreamColors$Companion; - public synthetic fun (JJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-0d7_KjU ()J public final fun component10-0d7_KjU ()J public final fun component11-0d7_KjU ()J @@ -1601,6 +1601,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public final fun component19-0d7_KjU ()J public final fun component2-0d7_KjU ()J public final fun component20-0d7_KjU ()J + public final fun component21-0d7_KjU ()J + public final fun component22-0d7_KjU ()J public final fun component3-0d7_KjU ()J public final fun component4-0d7_KjU ()J public final fun component5-0d7_KjU ()J @@ -1608,8 +1610,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public final fun component7-0d7_KjU ()J public final fun component8-0d7_KjU ()J public final fun component9-0d7_KjU ()J - public final fun copy-Cmkg8xs (JJJJJJJJJJJJJJJJJJJJ)Lio/getstream/chat/android/compose/ui/theme/StreamColors; - public static synthetic fun copy-Cmkg8xs$default (Lio/getstream/chat/android/compose/ui/theme/StreamColors;JJJJJJJJJJJJJJJJJJJJILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamColors; + public final fun copy-0VcbP8k (JJJJJJJJJJJJJJJJJJJJJJ)Lio/getstream/chat/android/compose/ui/theme/StreamColors; + public static synthetic fun copy-0VcbP8k$default (Lio/getstream/chat/android/compose/ui/theme/StreamColors;JJJJJJJJJJJJJJJJJJJJJJILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamColors; public fun equals (Ljava/lang/Object;)Z public final fun getAppBackground-0d7_KjU ()J public final fun getBarsBackground-0d7_KjU ()J @@ -1619,6 +1621,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public final fun getErrorAccent-0d7_KjU ()J public final fun getGiphyMessageBackground-0d7_KjU ()J public final fun getHighlight-0d7_KjU ()J + public final fun getImageBackgroundMessageList-0d7_KjU ()J + public final fun getImageBackgroundPickers-0d7_KjU ()J public final fun getInfoAccent-0d7_KjU ()J public final fun getInputBackground-0d7_KjU ()J public final fun getLinkBackground-0d7_KjU ()J diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index e1d1e3e29e3..72968378f11 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -54,6 +54,7 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.calculatePan @@ -108,6 +109,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView @@ -661,7 +663,12 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center ) { - ImagePlaceHolder(asyncImagePainterState = painter.state) + PlaceHolder( + asyncImagePainterState = painter.state, + isImage = attachment.type == AttachmentType.IMAGE, + progressIndicatorStrokeWidth = 6.dp, + progressIndicatorFillMaxSizePercentage = 0.2f + ) Image( modifier = transformModifier @@ -756,33 +763,44 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { * * @param asyncImagePainterState The painter state used to determine * which UI to show. + * @param isImage If the attachment we are holding the place for is + * a image or not. + * @param progressIndicatorStrokeWidth The thickness of the progress indicator + * used to indicate a loading thumbnail. + * @param progressIndicatorFillMaxSizePercentage Dictates what percentage of + * available parent size the progress indicator will fill. */ @Composable - private fun ImagePlaceHolder(asyncImagePainterState: AsyncImagePainter.State) { + private fun PlaceHolder( + asyncImagePainterState: AsyncImagePainter.State, + isImage: Boolean = false, + progressIndicatorStrokeWidth: Dp, + progressIndicatorFillMaxSizePercentage: Float, + ) { val painter = painterResource( id = R.drawable.stream_compose_ic_image_picker ) val imageModifier = Modifier.fillMaxSize(0.4f) - when (asyncImagePainterState) { - is AsyncImagePainter.State.Loading -> { + when { + asyncImagePainterState is AsyncImagePainter.State.Loading -> { CircularProgressIndicator( modifier = Modifier - .padding(horizontal = 8.dp) - .size(50.dp), - strokeWidth = 5.dp, + .padding(horizontal = 2.dp) + .fillMaxSize(progressIndicatorFillMaxSizePercentage), + strokeWidth = progressIndicatorStrokeWidth, color = ChatTheme.colors.primaryAccent ) } - is AsyncImagePainter.State.Error -> Icon( + asyncImagePainterState is AsyncImagePainter.State.Error && isImage -> Icon( tint = ChatTheme.colors.textLowEmphasis, modifier = imageModifier, painter = painter, contentDescription = null ) - is AsyncImagePainter.State.Success -> {} - is AsyncImagePainter.State.Empty -> { + asyncImagePainterState is AsyncImagePainter.State.Success -> {} + asyncImagePainterState is AsyncImagePainter.State.Empty && isImage -> { Icon( tint = ChatTheme.colors.textLowEmphasis, modifier = imageModifier, @@ -1253,9 +1271,15 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { user: User, pagerState: PagerState, ) { + // Used as a workaround for Coil's lack of a retry policy. + // See: https://github.com/coil-kt/coil/issues/884#issuecomment-975932886 + var retryHash by remember { + mutableStateOf(0) + } + val coroutineScope = rememberCoroutineScope() - BoxWithConstraints( + Box( modifier = Modifier .fillMaxWidth() .aspectRatio(1f) @@ -1267,20 +1291,48 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { }, contentAlignment = Alignment.Center ) { - val painter = rememberStreamImagePainter(attachment.imagePreviewUrl) + val painter = rememberStreamImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(attachment.imagePreviewUrl) + .setHeader("rety_hash", retryHash.toString()) + .build() + ) + + if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && painter.state is AsyncImagePainter.State.Error) { + retryHash++ + } Image( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .padding(1.dp) + .fillMaxSize() + .background(color = ChatTheme.colors.imageBackgroundMediaGalleryPicker), painter = painter, contentDescription = null, contentScale = ContentScale.Crop ) + PlaceHolder( + asyncImagePainterState = painter.state, + isImage = attachment.type == AttachmentType.IMAGE, + progressIndicatorStrokeWidth = 3.dp, + progressIndicatorFillMaxSizePercentage = 0.3f + ) + UserAvatar( modifier = Modifier .align(Alignment.TopStart) .padding(8.dp) - .size(24.dp), + .size(24.dp) + .border( + width = 1.dp, + color = Color.White, + shape = ChatTheme.shapes.avatar + ) + .shadow( + elevation = 5.dp, + shape = ChatTheme.shapes.avatar + ), user = user ) @@ -1289,10 +1341,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { modifier = Modifier .shadow(10.dp, shape = CircleShape) .background(color = Color.White, shape = CircleShape) - .size( - width = this@BoxWithConstraints.maxWidth / 6, - height = this@BoxWithConstraints.maxHeight / 6 - ) + .fillMaxSize(0.2f) ) } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt index b78564383f3..72f328f3c84 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt @@ -45,6 +45,10 @@ import io.getstream.chat.android.compose.R * @param giphyMessageBackground Used as a background for the ephemeral giphy messages. * @param threadSeparatorGradientStart Used as a start color for vertical gradient background in a thread separator. * @param threadSeparatorGradientEnd Used as an end color for vertical gradient background in a thread separator. + * @param imageBackgroundMessageList Used to set the background colour of images inside the message list. + * Most visible in placeholders before the images are loaded. + * @param imageBackgroundMediaGalleryPicker Used to set the background colour of images inside the media gallery picker + * in the media gallery preview screen. Most visible in placeholders before the images are loaded. */ @Immutable public data class StreamColors( @@ -68,6 +72,8 @@ public data class StreamColors( public val giphyMessageBackground: Color, public val threadSeparatorGradientStart: Color, public val threadSeparatorGradientEnd: Color, + public val imageBackgroundMessageList: Color, + public val imageBackgroundMediaGalleryPicker: Color ) { public companion object { @@ -98,6 +104,8 @@ public data class StreamColors( giphyMessageBackground = colorResource(R.color.stream_compose_bars_background), threadSeparatorGradientStart = colorResource(R.color.stream_compose_input_background), threadSeparatorGradientEnd = colorResource(R.color.stream_compose_app_background), + imageBackgroundMessageList = colorResource(R.color.stream_compose_text_low_emphasis), + imageBackgroundMediaGalleryPicker = colorResource(R.color.stream_compose_app_background), ) /** @@ -127,6 +135,8 @@ public data class StreamColors( giphyMessageBackground = colorResource(R.color.stream_compose_bars_background_dark), threadSeparatorGradientStart = colorResource(R.color.stream_compose_input_background_dark), threadSeparatorGradientEnd = colorResource(R.color.stream_compose_app_background_dark), + imageBackgroundMessageList = colorResource(R.color.stream_compose_text_low_emphasis_dark), + imageBackgroundMediaGalleryPicker = colorResource(R.color.stream_compose_app_background_dark), ) } } From 23a390e5d406a3ec6e1ca2d8dd9d717c1236f50a Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 1 Sep 2022 18:24:04 +0200 Subject: [PATCH 25/69] [3369] Improve the design and offline capabilities of `MediaGalleryPreviewOptions` --- stream-chat-android-compose/api/stream-chat-android-compose.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index e891e882909..ccd891a51fd 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1621,8 +1621,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public final fun getErrorAccent-0d7_KjU ()J public final fun getGiphyMessageBackground-0d7_KjU ()J public final fun getHighlight-0d7_KjU ()J + public final fun getImageBackgroundMediaGalleryPicker-0d7_KjU ()J public final fun getImageBackgroundMessageList-0d7_KjU ()J - public final fun getImageBackgroundPickers-0d7_KjU ()J public final fun getInfoAccent-0d7_KjU ()J public final fun getInputBackground-0d7_KjU ()J public final fun getLinkBackground-0d7_KjU ()J From 84d98fd13c8ce39372e8f3b2324c4b2b5e7ce011 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 1 Sep 2022 18:28:58 +0200 Subject: [PATCH 26/69] [3369] Switch to using an avatar without an online indicator we don't track that in real time in the gallery so it is pointless --- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 72968378f11..a2dd86573b9 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -129,6 +129,7 @@ import io.getstream.chat.android.client.models.Attachment import io.getstream.chat.android.client.models.ConnectionState import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.client.models.User +import io.getstream.chat.android.client.models.initials import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.handlers.DownloadPermissionHandler import io.getstream.chat.android.compose.handlers.PermissionHandler @@ -147,6 +148,7 @@ import io.getstream.chat.android.compose.ui.attachments.content.PlayButton import io.getstream.chat.android.compose.ui.components.LoadingIndicator import io.getstream.chat.android.compose.ui.components.NetworkLoadingIndicator import io.getstream.chat.android.compose.ui.components.Timestamp +import io.getstream.chat.android.compose.ui.components.avatar.Avatar import io.getstream.chat.android.compose.ui.components.avatar.UserAvatar import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter @@ -1319,7 +1321,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { progressIndicatorFillMaxSizePercentage = 0.3f ) - UserAvatar( + Avatar( modifier = Modifier .align(Alignment.TopStart) .padding(8.dp) @@ -1333,7 +1335,8 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { elevation = 5.dp, shape = ChatTheme.shapes.avatar ), - user = user + imageUrl = user.image, + initials = user.initials ) if (attachment.type == AttachmentType.VIDEO) { From 13bb2b2e5614a89db3953211840d88a293fac251 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 1 Sep 2022 18:29:37 +0200 Subject: [PATCH 27/69] [3369] Switch to using an avatar without an online indicator we don't track that in real time in the gallery so it is pointless --- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index a2dd86573b9..c3ea76ee2a5 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -149,7 +149,6 @@ import io.getstream.chat.android.compose.ui.components.LoadingIndicator import io.getstream.chat.android.compose.ui.components.NetworkLoadingIndicator import io.getstream.chat.android.compose.ui.components.Timestamp import io.getstream.chat.android.compose.ui.components.avatar.Avatar -import io.getstream.chat.android.compose.ui.components.avatar.UserAvatar import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter import io.getstream.chat.android.compose.viewmodel.mediapreview.MediaGalleryPreviewViewModel From 6aa7afaa3fbf89aecf8541616a0ceee37c1fec92 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 1 Sep 2022 18:30:47 +0200 Subject: [PATCH 28/69] [3369] Switch to using an avatar without an online indicator we don't track that in real time in the gallery so it is pointless --- stream-chat-android-compose/detekt-baseline.xml | 3 ++- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index 7bdc1cc1027..f082bb4c6fb 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -15,6 +15,7 @@ LongMethod:GroupAvatar.kt$@Composable public fun GroupAvatar( users: List<User>, modifier: Modifier = Modifier, shape: Shape = ChatTheme.shapes.avatar, textStyle: TextStyle = ChatTheme.typography.captionBold, onClick: (() -> Unit)? = null, ) LongMethod:ImageAttachmentContent.kt$@OptIn(ExperimentalFoundationApi::class) @Composable public fun ImageAttachmentContent( attachmentState: AttachmentState, modifier: Modifier = Modifier, ) LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun ImagePreviewContent( attachment: Attachment, pagerState: PagerState, page: Int, ) + LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun MediaGalleryItem( index: Int, attachment: Attachment, user: User, pagerState: PagerState, ) LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun VideoPreviewContent( attachment: Attachment, pagerState: PagerState, page: Int, onPlaybackError: () -> Unit, ) LongMethod:MessageComposer.kt$@Composable internal fun DefaultComposerIntegrations( messageInputState: MessageComposerState, onAttachmentsClick: () -> Unit, onCommandsClick: () -> Unit, ownCapabilities: Set<String>, ) LongMethod:MessageOptions.kt$@Composable public fun defaultMessageOptionsState( selectedMessage: Message, currentUser: User?, isInThread: Boolean, ownCapabilities: Set<String>, ): List<MessageOptionItemState> @@ -33,8 +34,8 @@ MagicNumber:MediaAttachmentContent.kt$0.85f MagicNumber:MediaAttachmentContent.kt$8 MagicNumber:MediaAttachmentFactory.kt$4 + MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$0.2f MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$0.4f - MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$6 MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$8f MagicNumber:Messages.kt$3 MagicNumber:Messages.kt$5 diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index c3ea76ee2a5..c666b2c4fe1 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -1299,7 +1299,8 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { .build() ) - if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && painter.state is AsyncImagePainter.State.Error) { + if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && + painter.state is AsyncImagePainter.State.Error) { retryHash++ } From d87ca74bbbf4ebd2e130598f9a0bf64c62f5716c Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Thu, 1 Sep 2022 18:31:02 +0200 Subject: [PATCH 29/69] [3369] Switch to using an avatar without an online indicator we don't track that in real time in the gallery so it is pointless --- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index c666b2c4fe1..f142ece0c88 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -1300,7 +1300,8 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { ) if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && - painter.state is AsyncImagePainter.State.Error) { + painter.state is AsyncImagePainter.State.Error + ) { retryHash++ } From b443c2d02ca6a1d795946222b7baf0099a4d2437 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Fri, 2 Sep 2022 10:58:22 +0200 Subject: [PATCH 30/69] [3369] Implement automatic retry for media content inside the message list. --- .../content/MediaAttachmentContent.kt | 44 ++++++++- .../preview/MediaGalleryPreviewActivity.kt | 63 +------------ .../ui/components/MediaPrviewPlaceHolder.kt | 92 +++++++++++++++++++ 3 files changed, 137 insertions(+), 62 deletions(-) create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/MediaPrviewPlaceHolder.kt diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 81a2ea61215..ec8f1c0e9a2 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -35,25 +35,35 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.Text import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImagePainter +import coil.request.ImageRequest import com.getstream.sdk.chat.utils.extensions.imagePreviewUrl +import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.models.Attachment +import io.getstream.chat.android.client.models.ConnectionState import io.getstream.chat.android.client.models.Message import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult import io.getstream.chat.android.compose.state.messages.attachments.AttachmentState import io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewContract +import io.getstream.chat.android.compose.ui.components.MediaPreviewPlaceHolder import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter import io.getstream.chat.android.uiutils.constant.AttachmentType @@ -299,13 +309,34 @@ internal fun MediaAttachmentContentItem( modifier: Modifier = Modifier, playButton: @Composable () -> Unit, ) { - val painter = rememberStreamImagePainter(attachment.imagePreviewUrl) + val connectionState by ChatClient.instance().clientState.connectionState.collectAsState() + + // Used as a workaround for Coil's lack of a retry policy. + // See: https://github.com/coil-kt/coil/issues/884#issuecomment-975932886 + var retryHash by remember { + mutableStateOf(0) + } + + val painter = rememberStreamImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(attachment.imagePreviewUrl) + .setParameter(key = "retry_hash", value = retryHash) + .build() + ) val mixedMediaPreviewLauncher = rememberLauncherForActivityResult( contract = MediaGalleryPreviewContract(), onResult = { result -> onMediaGalleryPreviewResult(result) } ) + // Used to refresh the request for the current page + // if it has previously failed. + if (connectionState == ConnectionState.CONNECTED && + painter.state is AsyncImagePainter.State.Error + ) { + retryHash++ + } + Box( modifier = modifier .background(Color.Black) @@ -327,12 +358,21 @@ internal fun MediaAttachmentContentItem( ) { Image( modifier = modifier - .fillMaxSize(), + .fillMaxSize() + .background(ChatTheme.colors.imageBackgroundMessageList), painter = painter, contentDescription = null, contentScale = ContentScale.Crop ) + MediaPreviewPlaceHolder( + asyncImagePainterState = painter.state, + progressIndicatorStrokeWidth = 3.dp, + progressIndicatorFillMaxSizePercentage = 0.25f, + isImage = attachment.type == AttachmentType.IMAGE, + placeholderIconTintColor = ChatTheme.colors.appBackground + ) + if (attachment.type == AttachmentType.VIDEO) { playButton() } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index f142ece0c88..25b05aadfc4 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -80,7 +80,6 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Scaffold @@ -109,7 +108,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView @@ -146,6 +144,7 @@ import io.getstream.chat.android.compose.state.mediagallerypreview.toMediaGaller import io.getstream.chat.android.compose.state.mediagallerypreview.toMessage import io.getstream.chat.android.compose.ui.attachments.content.PlayButton import io.getstream.chat.android.compose.ui.components.LoadingIndicator +import io.getstream.chat.android.compose.ui.components.MediaPreviewPlaceHolder import io.getstream.chat.android.compose.ui.components.NetworkLoadingIndicator import io.getstream.chat.android.compose.ui.components.Timestamp import io.getstream.chat.android.compose.ui.components.avatar.Avatar @@ -664,7 +663,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center ) { - PlaceHolder( + MediaPreviewPlaceHolder( asyncImagePainterState = painter.state, isImage = attachment.type == AttachmentType.IMAGE, progressIndicatorStrokeWidth = 6.dp, @@ -756,62 +755,6 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { } } - /** - * Displays an image icon if no image was loaded previously - * or the request has failed, a circular progress indicator - * if the image is loading or nothing if the image has successfully - * loaded. - * - * @param asyncImagePainterState The painter state used to determine - * which UI to show. - * @param isImage If the attachment we are holding the place for is - * a image or not. - * @param progressIndicatorStrokeWidth The thickness of the progress indicator - * used to indicate a loading thumbnail. - * @param progressIndicatorFillMaxSizePercentage Dictates what percentage of - * available parent size the progress indicator will fill. - */ - @Composable - private fun PlaceHolder( - asyncImagePainterState: AsyncImagePainter.State, - isImage: Boolean = false, - progressIndicatorStrokeWidth: Dp, - progressIndicatorFillMaxSizePercentage: Float, - ) { - val painter = painterResource( - id = R.drawable.stream_compose_ic_image_picker - ) - - val imageModifier = Modifier.fillMaxSize(0.4f) - - when { - asyncImagePainterState is AsyncImagePainter.State.Loading -> { - CircularProgressIndicator( - modifier = Modifier - .padding(horizontal = 2.dp) - .fillMaxSize(progressIndicatorFillMaxSizePercentage), - strokeWidth = progressIndicatorStrokeWidth, - color = ChatTheme.colors.primaryAccent - ) - } - asyncImagePainterState is AsyncImagePainter.State.Error && isImage -> Icon( - tint = ChatTheme.colors.textLowEmphasis, - modifier = imageModifier, - painter = painter, - contentDescription = null - ) - asyncImagePainterState is AsyncImagePainter.State.Success -> {} - asyncImagePainterState is AsyncImagePainter.State.Empty && isImage -> { - Icon( - tint = ChatTheme.colors.textLowEmphasis, - modifier = imageModifier, - painter = painter, - contentDescription = null - ) - } - } - } - /** * Represents an individual page containing video player with media controls. * @@ -1315,7 +1258,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { contentScale = ContentScale.Crop ) - PlaceHolder( + MediaPreviewPlaceHolder( asyncImagePainterState = painter.state, isImage = attachment.type == AttachmentType.IMAGE, progressIndicatorStrokeWidth = 3.dp, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/MediaPrviewPlaceHolder.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/MediaPrviewPlaceHolder.kt new file mode 100644 index 00000000000..0185647f297 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/MediaPrviewPlaceHolder.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.components + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImagePainter +import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.ui.theme.ChatTheme + +/** + * Displays an image icon if no image was loaded previously + * or the request has failed, a circular progress indicator + * if the image is loading or nothing if the image has successfully + * loaded. Does not show the image background or loading indicator + * if the media is a video attachment as it doesn't fit well along the + * play button. + * + * @param asyncImagePainterState The painter state used to determine + * which UI to show. + * @param isImage If the attachment we are holding the place for is + * a image or not. + * @param progressIndicatorStrokeWidth The thickness of the progress indicator + * used to indicate a loading thumbnail. + * @param progressIndicatorFillMaxSizePercentage Dictates what percentage of + * available parent size the progress indicator will fill. + * @param placeholderIconTintColor The tint of the place holder icon. + */ +@Composable +internal fun MediaPreviewPlaceHolder( + asyncImagePainterState: AsyncImagePainter.State, + isImage: Boolean = false, + progressIndicatorStrokeWidth: Dp, + progressIndicatorFillMaxSizePercentage: Float, + placeholderIconTintColor: Color = ChatTheme.colors.textLowEmphasis, + +) { + val painter = painterResource( + id = R.drawable.stream_compose_ic_image_picker + ) + + val imageModifier = Modifier.fillMaxSize(0.4f) + + when { + asyncImagePainterState is AsyncImagePainter.State.Loading -> { + CircularProgressIndicator( + modifier = Modifier + .padding(horizontal = 2.dp) + .fillMaxSize(progressIndicatorFillMaxSizePercentage), + strokeWidth = progressIndicatorStrokeWidth, + color = ChatTheme.colors.primaryAccent + ) + } + asyncImagePainterState is AsyncImagePainter.State.Error && isImage -> Icon( + tint = placeholderIconTintColor, + modifier = imageModifier, + painter = painter, + contentDescription = null + ) + asyncImagePainterState is AsyncImagePainter.State.Success -> {} + asyncImagePainterState is AsyncImagePainter.State.Empty && isImage -> { + Icon( + tint = placeholderIconTintColor, + modifier = imageModifier, + painter = painter, + contentDescription = null + ) + } + } +} From 9d7bcb923b725f9d64ed8d242ac11c3f08c54153 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Fri, 2 Sep 2022 12:17:06 +0200 Subject: [PATCH 31/69] [3369] Add an error Snackbar --- .../detekt-baseline.xml | 3 ++- .../preview/MediaGalleryPreviewActivity.kt | 20 +++++++++++++------ ...ceHolder.kt => MediaPreviewPlaceHolder.kt} | 0 .../src/main/res/values/strings.xml | 1 + 4 files changed, 17 insertions(+), 7 deletions(-) rename stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/{MediaPrviewPlaceHolder.kt => MediaPreviewPlaceHolder.kt} (100%) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index f082bb4c6fb..76d2146682e 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -17,6 +17,7 @@ LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun ImagePreviewContent( attachment: Attachment, pagerState: PagerState, page: Int, ) LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun MediaGalleryItem( index: Int, attachment: Attachment, user: User, pagerState: PagerState, ) LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun VideoPreviewContent( attachment: Attachment, pagerState: PagerState, page: Int, onPlaybackError: () -> Unit, ) + LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@OptIn(ExperimentalAnimationApi::class) @Composable private fun MediaGalleryPreviewContentWrapper( message: Message, initialAttachmentPosition: Int, ) LongMethod:MessageComposer.kt$@Composable internal fun DefaultComposerIntegrations( messageInputState: MessageComposerState, onAttachmentsClick: () -> Unit, onCommandsClick: () -> Unit, ownCapabilities: Set<String>, ) LongMethod:MessageOptions.kt$@Composable public fun defaultMessageOptionsState( selectedMessage: Message, currentUser: User?, isInThread: Boolean, ownCapabilities: Set<String>, ): List<MessageOptionItemState> LongMethod:Messages.kt$@Composable public fun Messages( messagesState: MessagesState, lazyListState: LazyListState, onMessagesStartReached: () -> Unit, onLastVisibleMessageChanged: (Message) -> Unit, onScrolledToBottom: () -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(vertical = 16.dp), helperContent: @Composable BoxScope.() -> Unit = { DefaultMessagesHelperContent(messagesState, lazyListState) }, loadingMoreContent: @Composable () -> Unit = { DefaultMessagesLoadingMoreIndicator() }, itemContent: @Composable (MessageListItemState) -> Unit, ) @@ -35,8 +36,8 @@ MagicNumber:MediaAttachmentContent.kt$8 MagicNumber:MediaAttachmentFactory.kt$4 MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$0.2f - MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$0.4f MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$8f + MagicNumber:MediaPreviewPlaceHolder.kt$0.4f MagicNumber:Messages.kt$3 MagicNumber:Messages.kt$5 MagicNumber:SearchInput.kt$8f diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 25b05aadfc4..e09a32f8175 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -85,6 +85,7 @@ import androidx.compose.material.IconButton import androidx.compose.material.Scaffold import androidx.compose.material.Surface import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -237,11 +238,14 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { val startingPosition = if (initialAttachmentPosition !in message.attachments.indices) 0 else initialAttachmentPosition + val scaffoldState = rememberScaffoldState() val pagerState = rememberPagerState(initialPage = startingPosition) + val coroutineScope = rememberCoroutineScope() Box(modifier = Modifier.fillMaxSize()) { Scaffold( modifier = Modifier.fillMaxSize(), + scaffoldState = scaffoldState, topBar = { MediaGalleryPreviewTopBar(message) }, content = { contentPadding -> if (message.id.isNotEmpty()) { @@ -251,7 +255,13 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { .fillMaxSize() .padding(contentPadding) ) { - MediaPreviewContent(pagerState, message.attachments) + MediaPreviewContent(pagerState, message.attachments) { + coroutineScope.launch { + scaffoldState.snackbarHostState.showSnackbar( + message = getString(R.string.stream_ui_message_list_video_display_error) + ) + } + } } } }, @@ -573,6 +583,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { private fun MediaPreviewContent( pagerState: PagerState, attachments: List, + onPlaybackError: () -> Unit, ) { if (attachments.isEmpty()) { finish() @@ -591,9 +602,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { attachment = attachments[page], pagerState = pagerState, page = page, - onPlaybackError = { - // TODO add error - } + onPlaybackError = onPlaybackError ) } } @@ -818,7 +827,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { } setOnPreparedListener { // Don't remove the preview unless the user has clicked play previously, - // otherwise the preview will be removed whenever the video finished downloading. + // otherwise the preview will be removed whenever the video has finished downloading. if (!hasPrepared && userHasClickedPlay) { shouldShowProgressBar = false shouldShowPreview = false @@ -843,7 +852,6 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { } Box(contentAlignment = Alignment.Center) { - AndroidView( modifier = Modifier .fillMaxSize() diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/MediaPrviewPlaceHolder.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/MediaPreviewPlaceHolder.kt similarity index 100% rename from stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/MediaPrviewPlaceHolder.kt rename to stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/MediaPreviewPlaceHolder.kt diff --git a/stream-chat-android-ui-common/src/main/res/values/strings.xml b/stream-chat-android-ui-common/src/main/res/values/strings.xml index f17f9eb3e88..8b6444271f8 100644 --- a/stream-chat-android-ui-common/src/main/res/values/strings.xml +++ b/stream-chat-android-ui-common/src/main/res/values/strings.xml @@ -34,6 +34,7 @@ The load failed due to the invalid url. Something went wrong. Unable to open attachment: %s Error. File can\'t be displayed + Error. Video can\'t be displayed There is no app to view this url:\n%s From 18f6019af61b17c260bb59c4e5a700ad30337d29 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Fri, 2 Sep 2022 15:26:17 +0200 Subject: [PATCH 32/69] [3369] Add a media quoted attachment factory and make minor and make minor UX improvements --- .../api/stream-chat-android-compose.api | 7 ++ .../content/MediaAttachmentContent.kt | 4 +- .../content/MediaAttachmentQuotedContent.kt | 88 +++++++++++++++++++ .../factory/MediaAttachmentFactory.kt | 34 +++---- .../factory/QuotedAttachmentFactory.kt | 5 +- .../preview/MediaGalleryPreviewActivity.kt | 6 +- .../android/compose/ui/theme/StreamColors.kt | 6 +- 7 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index ccd891a51fd..966c37e3350 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -611,12 +611,17 @@ public final class io/getstream/chat/android/compose/ui/attachments/content/Link public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContentKt { public static final fun MediaAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;ILkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V + public static final fun PlayButton (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContentKt { public static final fun MediaAttachmentPreviewContent (Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } +public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContentKt { + public static final fun MediaAttachmentQuotedContent (Lio/getstream/chat/android/client/models/Attachment;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + public final class io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContentKt { public static final fun MessageAttachmentsContent (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } @@ -653,8 +658,10 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Comp public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$QuotedAttachmentFactoryKt { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index ec8f1c0e9a2..261502110dc 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -370,7 +370,7 @@ internal fun MediaAttachmentContentItem( progressIndicatorStrokeWidth = 3.dp, progressIndicatorFillMaxSizePercentage = 0.25f, isImage = attachment.type == AttachmentType.IMAGE, - placeholderIconTintColor = ChatTheme.colors.appBackground + placeholderIconTintColor = ChatTheme.colors.disabled ) if (attachment.type == AttachmentType.VIDEO) { @@ -386,7 +386,7 @@ internal fun MediaAttachmentContentItem( * @param modifier The modifier used for styling. */ @Composable -internal fun PlayButton( +public fun PlayButton( modifier: Modifier = Modifier, ) { Box( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt new file mode 100644 index 00000000000..bd16662db46 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2014-2022 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.attachments.content + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.getstream.sdk.chat.utils.extensions.imagePreviewUrl +import io.getstream.chat.android.client.models.Attachment +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter +import io.getstream.chat.android.uiutils.constant.AttachmentType + +/** + * Builds an image attachment for a quoted message which is composed from a singe attachment previewing the attached + * image, link preview or giphy. + * + * @param attachment The attachment we wish to show to users. + * @param modifier Modifier for styling. + */ +@Composable +public fun MediaAttachmentQuotedContent( + attachment: Attachment, + modifier: Modifier = Modifier, +) { + val imagePainter = rememberStreamImagePainter(attachment.imagePreviewUrl) + + Box( + modifier = modifier + .padding( + start = ChatTheme.dimens.quotedMessageAttachmentStartPadding, + top = ChatTheme.dimens.quotedMessageAttachmentTopPadding, + bottom = ChatTheme.dimens.quotedMessageAttachmentBottomPadding, + end = ChatTheme.dimens.quotedMessageAttachmentEndPadding + ) + .size(ChatTheme.dimens.quotedMessageAttachmentPreviewSize) + .clip(ChatTheme.shapes.quotedAttachment), + contentAlignment = Alignment.Center + ) { + + Image( + modifier = Modifier + .fillMaxSize(1f), + painter = imagePainter, + contentDescription = null, + contentScale = ContentScale.Crop + ) + + if (attachment.type == AttachmentType.VIDEO) { + PlayButton( + modifier = Modifier + .padding(10.dp) + .shadow(6.dp, shape = CircleShape) + .background(color = Color.White, shape = CircleShape) + .fillMaxWidth(0.8f) + .aspectRatio(1f) + ) + } + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt index 55f797dbba4..29bfd3c2111 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt @@ -17,8 +17,10 @@ package io.getstream.chat.android.compose.ui.attachments.factory import androidx.compose.foundation.background -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable @@ -29,7 +31,6 @@ import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentContent import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentPreviewContent -import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentPreviewItemSize import io.getstream.chat.android.compose.ui.attachments.content.PlayButton import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.uiutils.constant.AttachmentType @@ -48,9 +49,7 @@ import io.getstream.chat.android.uiutils.constant.AttachmentType public fun MediaAttachmentFactory( maximumNumberOfPreviewedItems: Int = 4, contentPlayButton: @Composable () -> Unit = { - DefaultContentPlayButton( - maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems - ) + DefaultContentPlayButton() }, previewContentPlayButton: @Composable () -> Unit = { DefaultPreviewContentPlayButton() @@ -85,22 +84,16 @@ public fun MediaAttachmentFactory( * Represents the default play button that is * overlaid above video attachment previews inside * the messages list. - * - * @param maximumNumberOfPreviewedItems The maximum number of thumbnails that can be displayed - * in a group when previewing Media attachments in the message list. */ @Composable -private fun DefaultContentPlayButton( - maximumNumberOfPreviewedItems: Int, -) { +private fun DefaultContentPlayButton() { PlayButton( modifier = Modifier - .shadow(10.dp, shape = CircleShape) + .padding(2.dp) + .shadow(6.dp, shape = CircleShape) .background(color = Color.White, shape = CircleShape) - .size( - width = ChatTheme.dimens.attachmentsContentImageWidth / maximumNumberOfPreviewedItems, - height = ChatTheme.dimens.attachmentsContentImageWidth / maximumNumberOfPreviewedItems, - ) + .fillMaxWidth(0.25f) + .aspectRatio(1f) ) } @@ -113,11 +106,8 @@ private fun DefaultContentPlayButton( private fun DefaultPreviewContentPlayButton() { PlayButton( modifier = Modifier - .shadow(10.dp, shape = CircleShape) + .shadow(6.dp, shape = CircleShape) .background(color = Color.White, shape = CircleShape) - .size( - width = (MediaAttachmentPreviewItemSize / 4).dp, - height = (MediaAttachmentPreviewItemSize / 4).dp - ) + .fillMaxSize(0.25f) ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt index 18014eb0dd3..302d49bec1c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt @@ -20,7 +20,9 @@ import androidx.compose.runtime.Composable import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory import io.getstream.chat.android.compose.ui.attachments.content.FileAttachmentQuotedContent import io.getstream.chat.android.compose.ui.attachments.content.ImageAttachmentQuotedContent +import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentQuotedContent import io.getstream.chat.android.compose.ui.util.isMedia +import io.getstream.chat.android.uiutils.constant.AttachmentType import io.getstream.chat.android.uiutils.extension.hasLink import io.getstream.chat.android.uiutils.extension.isFile @@ -40,11 +42,12 @@ public fun QuotedAttachmentFactory(): AttachmentFactory = AttachmentFactory( val attachment = attachmentState.message.attachments.first() val isFile = attachment.isFile() + val isVideo = attachment.type == AttachmentType.VIDEO val isImage = attachment.isMedia() val isLink = attachment.hasLink() when { - isImage || isLink -> ImageAttachmentQuotedContent(modifier = modifier, attachment = attachment) + isImage || isVideo || isLink -> MediaAttachmentQuotedContent(modifier = modifier, attachment = attachment) isFile -> FileAttachmentQuotedContent(modifier = modifier, attachment = attachment) } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index e09a32f8175..a4aa4cd48d4 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -889,7 +889,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { if (shouldShowPlayButton) { PlayButton( modifier = Modifier - .shadow(10.dp, shape = CircleShape) + .shadow(6.dp, shape = CircleShape) .background(color = Color.White, shape = CircleShape) .size( width = 42.dp, @@ -1284,7 +1284,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { shape = ChatTheme.shapes.avatar ) .shadow( - elevation = 5.dp, + elevation = 4.dp, shape = ChatTheme.shapes.avatar ), imageUrl = user.image, @@ -1294,7 +1294,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { if (attachment.type == AttachmentType.VIDEO) { PlayButton( modifier = Modifier - .shadow(10.dp, shape = CircleShape) + .shadow(6.dp, shape = CircleShape) .background(color = Color.White, shape = CircleShape) .fillMaxSize(0.2f) ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt index 72f328f3c84..199e6a999b6 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt @@ -73,7 +73,7 @@ public data class StreamColors( public val threadSeparatorGradientStart: Color, public val threadSeparatorGradientEnd: Color, public val imageBackgroundMessageList: Color, - public val imageBackgroundMediaGalleryPicker: Color + public val imageBackgroundMediaGalleryPicker: Color, ) { public companion object { @@ -104,7 +104,7 @@ public data class StreamColors( giphyMessageBackground = colorResource(R.color.stream_compose_bars_background), threadSeparatorGradientStart = colorResource(R.color.stream_compose_input_background), threadSeparatorGradientEnd = colorResource(R.color.stream_compose_app_background), - imageBackgroundMessageList = colorResource(R.color.stream_compose_text_low_emphasis), + imageBackgroundMessageList = colorResource(R.color.stream_compose_input_background), imageBackgroundMediaGalleryPicker = colorResource(R.color.stream_compose_app_background), ) @@ -135,7 +135,7 @@ public data class StreamColors( giphyMessageBackground = colorResource(R.color.stream_compose_bars_background_dark), threadSeparatorGradientStart = colorResource(R.color.stream_compose_input_background_dark), threadSeparatorGradientEnd = colorResource(R.color.stream_compose_app_background_dark), - imageBackgroundMessageList = colorResource(R.color.stream_compose_text_low_emphasis_dark), + imageBackgroundMessageList = colorResource(R.color.stream_compose_input_background_dark), imageBackgroundMediaGalleryPicker = colorResource(R.color.stream_compose_app_background_dark), ) } From c450208189cba088368bb01664ed01e1ccfd8c25 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Fri, 2 Sep 2022 15:32:21 +0200 Subject: [PATCH 33/69] [3369] Add a media quoted attachment factory and make minor and make minor UX improvements --- stream-chat-android-compose/detekt-baseline.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index 76d2146682e..41790775a46 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -34,7 +34,8 @@ MagicNumber:ImagePreviewActivity.kt$ImagePreviewActivity$8f MagicNumber:MediaAttachmentContent.kt$0.85f MagicNumber:MediaAttachmentContent.kt$8 - MagicNumber:MediaAttachmentFactory.kt$4 + MagicNumber:MediaAttachmentFactory.kt$0.25f + MagicNumber:MediaAttachmentQuotedContent.kt$0.8f MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$0.2f MagicNumber:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$8f MagicNumber:MediaPreviewPlaceHolder.kt$0.4f From 532afa3a8e556bb1a0d1c5131f027325bf6a0ea2 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 5 Sep 2022 13:47:12 +0200 Subject: [PATCH 34/69] [3369] Disable clicking on disabled options --- .../MediaGalleryPreviewOption.kt | 2 ++ .../preview/MediaGalleryPreviewActivity.kt | 30 ++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewOption.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewOption.kt index 55d7e43b32e..38075a2b5d5 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewOption.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewOption.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.painter.Painter * @param iconPainter The icon of the option. * @param iconColor The color of the icon. * @param action The action this option represents. + * @param isEnabled If the action is currently enabled. */ internal data class MediaGalleryPreviewOption( internal val title: String, @@ -34,4 +35,5 @@ internal data class MediaGalleryPreviewOption( internal val iconPainter: Painter, internal val iconColor: Color, internal val action: MediaGalleryPreviewAction, + internal val isEnabled: Boolean, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index a4aa4cd48d4..eca51f4187a 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -89,6 +89,7 @@ import androidx.compose.material.rememberScaffoldState import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -481,6 +482,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { modifier = Modifier .fillMaxWidth() .background(ChatTheme.colors.barsBackground) + .padding(8.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), @@ -491,9 +493,9 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { pagerState.currentPage, downloadPermissionHandler ) - } - ) - .padding(8.dp), + }, + enabled = mediaGalleryPreviewOption.isEnabled + ), verticalAlignment = Alignment.CenterVertically ) { Spacer(modifier = Modifier.width(8.dp)) @@ -958,6 +960,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { @Composable private fun MediaGalleryPreviewBottomBar(attachments: List, pagerState: PagerState) { val attachmentCount = attachments.size + val isCurrentAttachmentVideo = attachments[pagerState.currentPage].type == AttachmentType.VIDEO Surface( modifier = Modifier @@ -974,12 +977,12 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { IconButton( modifier = Modifier.align(Alignment.CenterStart), onClick = { onShareMediaClick(attachments[pagerState.currentPage]) }, - enabled = mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED + enabled = mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && !isCurrentAttachmentVideo ) { Icon( painter = painterResource(id = R.drawable.stream_compose_ic_share), contentDescription = stringResource(id = R.string.stream_compose_image_preview_share), - tint = if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED) { + tint = if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && !isCurrentAttachmentVideo) { ChatTheme.colors.textHighEmphasis } else { ChatTheme.colors.disabled @@ -1022,8 +1025,15 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { @Composable private fun defaultMediaOptions(message: Message): List { val user by mediaGalleryPreviewViewModel.user.collectAsState() + + val isChatConnected by remember(mediaGalleryPreviewViewModel.connectionState) { + derivedStateOf { + mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED + } + } + val saveMediaColor = - if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED) { + if (isChatConnected) { ChatTheme.colors.textHighEmphasis } else { ChatTheme.colors.disabled @@ -1035,14 +1045,16 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { titleColor = ChatTheme.colors.textHighEmphasis, iconPainter = painterResource(id = R.drawable.stream_compose_ic_reply), iconColor = ChatTheme.colors.textHighEmphasis, - action = Reply(message) + action = Reply(message), + isEnabled = true ), MediaGalleryPreviewOption( title = stringResource(id = R.string.stream_compose_media_gallery_preview_show_in_chat), titleColor = ChatTheme.colors.textHighEmphasis, iconPainter = painterResource(id = R.drawable.stream_compose_ic_show_in_chat), iconColor = ChatTheme.colors.textHighEmphasis, - action = ShowInChat(message) + action = ShowInChat(message), + isEnabled = true ), MediaGalleryPreviewOption( title = stringResource(id = R.string.stream_compose_media_gallery_preview_save_image), @@ -1050,6 +1062,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { iconPainter = painterResource(id = R.drawable.stream_compose_ic_download), iconColor = saveMediaColor, action = SaveMedia(message), + isEnabled = isChatConnected ) ) @@ -1068,6 +1081,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { iconPainter = painterResource(id = R.drawable.stream_compose_ic_delete), iconColor = deleteColor, action = Delete(message), + isEnabled = isChatConnected ) ) } From 76920744fff140a91923437768e0024528189fb6 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 5 Sep 2022 13:47:55 +0200 Subject: [PATCH 35/69] [3369] Appease Detekt --- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index eca51f4187a..42a59a58012 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -977,12 +977,15 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { IconButton( modifier = Modifier.align(Alignment.CenterStart), onClick = { onShareMediaClick(attachments[pagerState.currentPage]) }, - enabled = mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && !isCurrentAttachmentVideo + enabled = mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED + && !isCurrentAttachmentVideo ) { Icon( painter = painterResource(id = R.drawable.stream_compose_ic_share), contentDescription = stringResource(id = R.string.stream_compose_image_preview_share), - tint = if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && !isCurrentAttachmentVideo) { + tint = if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED + && !isCurrentAttachmentVideo + ) { ChatTheme.colors.textHighEmphasis } else { ChatTheme.colors.disabled From 990574a4e50fc665981993456bbe36d545187ea8 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 5 Sep 2022 13:49:13 +0200 Subject: [PATCH 36/69] [3369] Appease Detekt --- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 42a59a58012..207478cf292 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -977,14 +977,14 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { IconButton( modifier = Modifier.align(Alignment.CenterStart), onClick = { onShareMediaClick(attachments[pagerState.currentPage]) }, - enabled = mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED - && !isCurrentAttachmentVideo + enabled = mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && + !isCurrentAttachmentVideo ) { Icon( painter = painterResource(id = R.drawable.stream_compose_ic_share), contentDescription = stringResource(id = R.string.stream_compose_image_preview_share), - tint = if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED - && !isCurrentAttachmentVideo + tint = if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && + !isCurrentAttachmentVideo ) { ChatTheme.colors.textHighEmphasis } else { From 2c559e970cb1bf82a49647a8bfeb71eb17433e73 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 5 Sep 2022 14:07:11 +0200 Subject: [PATCH 37/69] [3369] Check if the request data is null when retrying requests to avoid recomposition loops --- .../ui/attachments/content/MediaAttachmentContent.kt | 5 +++-- .../attachments/preview/MediaGalleryPreviewActivity.kt | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 261502110dc..c4d9899096a 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -317,9 +317,10 @@ internal fun MediaAttachmentContentItem( mutableStateOf(0) } + val data = attachment.imagePreviewUrl val painter = rememberStreamImagePainter( model = ImageRequest.Builder(LocalContext.current) - .data(attachment.imagePreviewUrl) + .data(data) .setParameter(key = "retry_hash", value = retryHash) .build() ) @@ -331,7 +332,7 @@ internal fun MediaAttachmentContentItem( // Used to refresh the request for the current page // if it has previously failed. - if (connectionState == ConnectionState.CONNECTED && + if (data != null && connectionState == ConnectionState.CONNECTED && painter.state is AsyncImagePainter.State.Error ) { retryHash++ diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 207478cf292..8f0ddb3ecfc 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -634,10 +634,11 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { mutableStateOf(0) } + val data = attachment.imagePreviewUrl val painter = rememberStreamImagePainter( model = ImageRequest.Builder(LocalContext.current) - .data(attachment.imagePreviewUrl) + .data(data) .crossfade(true) .setParameter(key = "retry_hash", value = retryHash) .build() @@ -654,7 +655,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { // Used to refresh the request for the current page // if it has previously failed. - if (page == pagerState.currentPage && + if (data != null && page == pagerState.currentPage && mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && painter.state is AsyncImagePainter.State.Error ) { @@ -1260,14 +1261,15 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { }, contentAlignment = Alignment.Center ) { + val data = attachment.imagePreviewUrl val painter = rememberStreamImagePainter( model = ImageRequest.Builder(LocalContext.current) .data(attachment.imagePreviewUrl) - .setHeader("rety_hash", retryHash.toString()) + .setHeader("retry_hash", retryHash.toString()) .build() ) - if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && + if (data != null && mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && painter.state is AsyncImagePainter.State.Error ) { retryHash++ From 6ede2148cd7692e132e29fae37f4723d27eb7faa Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 5 Sep 2022 14:08:23 +0200 Subject: [PATCH 38/69] [3369] Generate detekt baseline --- stream-chat-android-compose/detekt-baseline.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index 41790775a46..55e8d152405 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -2,6 +2,7 @@ + ComplexCondition:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$data != null && page == pagerState.currentPage && mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && painter.state is AsyncImagePainter.State.Error ComplexCondition:MessageItem.kt$!messageItem.isMine && ( messageItem.shouldShowFooter || messageItem.groupPosition == Bottom || messageItem.groupPosition == None ) ComplexCondition:MessageOptions.kt$((isOwnMessage && canEditOwnMessage) || canEditAnyMessage) && !selectedMessage.isGiphy() ComplexCondition:Messages.kt$!endOfMessages && index == messages.lastIndex && messages.isNotEmpty() && lazyListState.isScrollInProgress @@ -14,6 +15,7 @@ LongMethod:GiphyMessageContent.kt$@Composable public fun GiphyMessageContent( message: Message, modifier: Modifier = Modifier, onGiphyActionClick: (GiphyAction) -> Unit = {}, ) LongMethod:GroupAvatar.kt$@Composable public fun GroupAvatar( users: List<User>, modifier: Modifier = Modifier, shape: Shape = ChatTheme.shapes.avatar, textStyle: TextStyle = ChatTheme.typography.captionBold, onClick: (() -> Unit)? = null, ) LongMethod:ImageAttachmentContent.kt$@OptIn(ExperimentalFoundationApi::class) @Composable public fun ImageAttachmentContent( attachmentState: AttachmentState, modifier: Modifier = Modifier, ) + LongMethod:MediaAttachmentContent.kt$@Suppress("LongParameterList") @OptIn(ExperimentalFoundationApi::class) @Composable internal fun MediaAttachmentContentItem( message: Message, attachmentPosition: Int, attachment: Attachment, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit, onLongItemClick: (Message) -> Unit, modifier: Modifier = Modifier, playButton: @Composable () -> Unit, ) LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun ImagePreviewContent( attachment: Attachment, pagerState: PagerState, page: Int, ) LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun MediaGalleryItem( index: Int, attachment: Attachment, user: User, pagerState: PagerState, ) LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun VideoPreviewContent( attachment: Attachment, pagerState: PagerState, page: Int, onPlaybackError: () -> Unit, ) From ff19d96c4961adeb171efc7f4783844eadc70b74 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 5 Sep 2022 14:50:37 +0200 Subject: [PATCH 39/69] [3369] Add the ability to disable video thumbnails --- .../compose/sample/ui/MessagesActivity.kt | 5 ++- .../api/stream-chat-android-compose.api | 10 +++-- .../content/MediaAttachmentContent.kt | 15 +++++++- .../preview/MediaGalleryPreviewActivity.kt | 38 ++++++++++++++++--- .../preview/MediaGalleryPreviewContract.kt | 5 ++- .../android/compose/ui/theme/ChatTheme.kt | 19 +++++++++- 6 files changed, 78 insertions(+), 14 deletions(-) diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt index 6d65edd2d6e..218b5c603d5 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt @@ -93,7 +93,10 @@ class MessagesActivity : BaseConnectedActivity() { val channelId = intent.getStringExtra(KEY_CHANNEL_ID) ?: return setContent { - ChatTheme(dateFormatter = ChatApp.dateFormatter) { + ChatTheme( + dateFormatter = ChatApp.dateFormatter, + videoThumbnailsEnabled = false + ) { MessagesScreen( channelId = channelId, onBackPressed = { finish() }, diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 966c37e3350..b0c90d26789 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -770,7 +770,7 @@ public final class io/getstream/chat/android/compose/ui/attachments/preview/Medi } public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity$Companion { - public final fun getIntent (Landroid/content/Context;Lio/getstream/chat/android/client/models/Message;I)Landroid/content/Intent; + public final fun getIntent (Landroid/content/Context;Lio/getstream/chat/android/client/models/Message;IZ)Landroid/content/Intent; } public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract : androidx/activity/result/contract/ActivityResultContract { @@ -784,10 +784,11 @@ public final class io/getstream/chat/android/compose/ui/attachments/preview/Medi public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract$Input { public static final field $stable I - public fun (Lio/getstream/chat/android/client/models/Message;I)V - public synthetic fun (Lio/getstream/chat/android/client/models/Message;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/client/models/Message;IZ)V + public synthetic fun (Lio/getstream/chat/android/client/models/Message;IZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getInitialPosition ()I public final fun getMessage ()Lio/getstream/chat/android/client/models/Message; + public final fun getVideoThumbnailsEnabled ()Z } public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity : androidx/appcompat/app/AppCompatActivity { @@ -1586,10 +1587,11 @@ public final class io/getstream/chat/android/compose/ui/theme/ChatTheme { public final fun getReactionIconFactory (Landroidx/compose/runtime/Composer;I)Lio/getstream/chat/android/compose/ui/util/ReactionIconFactory; public final fun getShapes (Landroidx/compose/runtime/Composer;I)Lio/getstream/chat/android/compose/ui/theme/StreamShapes; public final fun getTypography (Landroidx/compose/runtime/Composer;I)Lio/getstream/chat/android/compose/ui/theme/StreamTypography; + public final fun getVideoThumbnailsEnabled (Landroidx/compose/runtime/Composer;I)Z } public final class io/getstream/chat/android/compose/ui/theme/ChatThemeKt { - public static final fun ChatTheme (ZLio/getstream/chat/android/compose/ui/theme/StreamColors;Lio/getstream/chat/android/compose/ui/theme/StreamDimens;Lio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamShapes;Landroidx/compose/material/ripple/RippleTheme;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/compose/ui/util/ReactionIconFactory;Lcom/getstream/sdk/chat/utils/DateFormatter;Lio/getstream/chat/android/compose/ui/util/ChannelNameFormatter;Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter;Lio/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory;Lio/getstream/chat/android/compose/ui/util/MessageAlignmentProvider;Lio/getstream/chat/android/common/MessageOptionsUserReactionAlignment;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V + public static final fun ChatTheme (ZLio/getstream/chat/android/compose/ui/theme/StreamColors;Lio/getstream/chat/android/compose/ui/theme/StreamDimens;Lio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamShapes;Landroidx/compose/material/ripple/RippleTheme;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/compose/ui/util/ReactionIconFactory;Lcom/getstream/sdk/chat/utils/DateFormatter;Lio/getstream/chat/android/compose/ui/util/ChannelNameFormatter;Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter;Lio/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory;Lio/getstream/chat/android/compose/ui/util/MessageAlignmentProvider;Lio/getstream/chat/android/common/MessageOptionsUserReactionAlignment;Ljava/util/List;Ljava/util/List;ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/chat/android/compose/ui/theme/StreamColors { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index c4d9899096a..86ca566bcc0 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -317,7 +317,15 @@ internal fun MediaAttachmentContentItem( mutableStateOf(0) } - val data = attachment.imagePreviewUrl + val data = + if (attachment.type == AttachmentType.IMAGE || + (attachment.type == AttachmentType.VIDEO && ChatTheme.videoThumbnailsEnabled) + ) { + attachment.imagePreviewUrl + } else { + null + } + val painter = rememberStreamImagePainter( model = ImageRequest.Builder(LocalContext.current) .data(data) @@ -338,6 +346,8 @@ internal fun MediaAttachmentContentItem( retryHash++ } + val areVideosEnabled = ChatTheme.videoThumbnailsEnabled + Box( modifier = modifier .background(Color.Black) @@ -349,7 +359,8 @@ internal fun MediaAttachmentContentItem( mixedMediaPreviewLauncher.launch( MediaGalleryPreviewContract.Input( message = message, - initialPosition = attachmentPosition + initialPosition = attachmentPosition, + videoThumbnailsEnabled = areVideosEnabled ) ) }, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 8f0ddb3ecfc..a7943a4b8b0 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -193,6 +193,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { val mediaGalleryPreviewActivityState = intent?.getParcelableExtra( KeyMediaGalleryPreviewActivityState ) + val videoThumbnailsEnabled = intent?.getBooleanExtra(KeyVideoThumbnailsEnabled, true) ?: true val messageId = mediaGalleryPreviewActivityState?.messageId ?: "" if (!mediaGalleryPreviewViewModel.hasCompleteMessage) { @@ -209,7 +210,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { } setContent { - ChatTheme { + ChatTheme(videoThumbnailsEnabled = videoThumbnailsEnabled) { val message = mediaGalleryPreviewViewModel.message if (message.deletedAt != null) { @@ -863,7 +864,13 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { ) if (shouldShowPreview) { - val painter = rememberStreamImagePainter(data = attachment.thumbUrl) + val data = if (ChatTheme.videoThumbnailsEnabled) { + attachment.thumbUrl + } else { + null + } + + val painter = rememberStreamImagePainter(data = data) Box( contentAlignment = Alignment.Center, @@ -1261,10 +1268,18 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { }, contentAlignment = Alignment.Center ) { - val data = attachment.imagePreviewUrl + val data = + if (attachment.type == AttachmentType.IMAGE || + (attachment.type == AttachmentType.VIDEO && ChatTheme.videoThumbnailsEnabled) + ) { + attachment.imagePreviewUrl + } else { + null + } + val painter = rememberStreamImagePainter( model = ImageRequest.Builder(LocalContext.current) - .data(attachment.imagePreviewUrl) + .data(data) .setHeader("retry_hash", retryHash.toString()) .build() ) @@ -1332,6 +1347,12 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { */ private const val KeyMediaGalleryPreviewActivityState: String = "mediaGalleryPreviewActivityState" + /** + * Represents the key for the [Boolean] value dictating whether video thumbnails + * will be displayed in previews or not. + */ + private const val KeyVideoThumbnailsEnabled: String = "videoThumbnailsEnabled" + /** * Represents the key for the starting attachment position based on the clicked attachment. */ @@ -1368,13 +1389,20 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { * @param context The context to start the activity with. * @param message The [Message] containing the attachments. * @param attachmentPosition The initial position of the clicked media attachment. + * @param videoThumbnailsEnabled Whether video thumbnails will be displayed in previews or not. */ - public fun getIntent(context: Context, message: Message, attachmentPosition: Int): Intent { + public fun getIntent( + context: Context, + message: Message, + attachmentPosition: Int, + videoThumbnailsEnabled: Boolean, + ): Intent { return Intent(context, MediaGalleryPreviewActivity::class.java).apply { val mediaGalleryPreviewActivityState = message.toMediaGalleryPreviewActivityState() putExtra(KeyMediaGalleryPreviewActivityState, mediaGalleryPreviewActivityState) putExtra(KeyAttachmentPosition, attachmentPosition) + putExtra(KeyVideoThumbnailsEnabled, videoThumbnailsEnabled) } } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt index 3ab6b7eca65..4c2ce4b5ace 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt @@ -38,7 +38,8 @@ public class MediaGalleryPreviewContract : return MediaGalleryPreviewActivity.getIntent( context, message = input.message, - attachmentPosition = input.initialPosition + attachmentPosition = input.initialPosition, + videoThumbnailsEnabled = input.videoThumbnailsEnabled ) } @@ -56,9 +57,11 @@ public class MediaGalleryPreviewContract : * * @param message The message containing the attachments. * @param initialPosition The initial position of the Image gallery, based on the clicked item. + * @param videoThumbnailsEnabled Whether video thumbnails will be displayed in previews or not. */ public class Input( public val message: Message, public val initialPosition: Int = 0, + public val videoThumbnailsEnabled: Boolean, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatTheme.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatTheme.kt index 8965777dcef..19c3b92e575 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatTheme.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatTheme.kt @@ -100,6 +100,12 @@ private val LocalPermissionManagerProvider = compositionLocalOf> { error("No attachments picker tab factories provided! Make sure to wrap all usages of Stream components in a ChatTheme.") } +private val LocalVideoThumbnailsEnabled = compositionLocalOf { + error( + "No videoThumbnailsEnabled Boolean provided! " + + "Make sure to wrap all usages of Stream components in a ChatTheme." + ) +} /** * Our theme that provides all the important properties for styling to the user. @@ -123,6 +129,7 @@ private val LocalAttachmentsPickerTabFactories = compositionLocalOf = AttachmentsPickerTabFactories.defaultFactories(), + videoThumbnailsEnabled: Boolean = true, content: @Composable () -> Unit, ) { LaunchedEffect(Unit) { @@ -178,6 +186,7 @@ public fun ChatTheme( LocalMessageOptionsUserReactionAlignment provides messageOptionsUserReactionAlignment, LocalPermissionManagerProvider provides permissionHandlers, LocalAttachmentsPickerTabFactories provides attachmentsPickerTabFactories, + LocalVideoThumbnailsEnabled provides videoThumbnailsEnabled, ) { content() } @@ -301,11 +310,19 @@ public object ChatTheme { get() = LocalPermissionManagerProvider.current /** - * * Retrieves the current list of [AttachmentsPickerTabFactory] at the call site's position in the hierarchy. */ public val attachmentsPickerTabFactories: List @Composable @ReadOnlyComposable get() = LocalAttachmentsPickerTabFactories.current + + /** + * Retrieves the value of [Boolean] dictating whether video thumbnails are enabled at the call site's + * position in the hierarchy. + */ + public val videoThumbnailsEnabled: Boolean + @Composable + @ReadOnlyComposable + get() = LocalVideoThumbnailsEnabled.current } From 8a775f9528fb951f7d1855e964e0fad0e93db95d Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 5 Sep 2022 16:31:04 +0200 Subject: [PATCH 40/69] [3369] Start the deprecation process for replaced components. --- DEPRECATIONS.md | 4 ++++ .../content/ImageAttachmentContent.kt | 10 +++++++++ .../factory/ImageAttachmentFactory.kt | 9 ++++++++ .../preview/ImagePreviewActivity.kt | 20 +++++++++++++++++ .../preview/ImagePreviewContract.kt | 22 +++++++++++++++++++ 5 files changed, 65 insertions(+) diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index d0f4f6ad7ec..fac70e325e4 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -4,6 +4,10 @@ This document lists deprecated constructs in the SDK, with their expected time | API / Feature | Deprecated (warning) | Deprecated (error) | Removed | Notes | | --- | --- | --- | --- | --- | +| `ImageAttachmentContent` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | `ImageAttachmentContent` has been deprecated in favor of `MediattachmentContent`. The new function is able to preview videos as well as images and has access to a new and improved media gallery. | +| `ImageAttachmentFactory` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | `ImageAttachmentFactory` has been deprecated in favor of `MediaAttachmentFactory`. The new factory is able to preview videos as well as images and has access to a new and improved media gallery. | +| `ImagePreviewContract` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | `ImagePreviewContract` has been deprecated in favor of `MediaGalleryPreviewContract`, please use it in conjunction with `MediaGalleryPreviewActivity`. The new gallery holds multiple improvements such as the ability to reproduce mixed image and video content, automatic reloading upon regaining network connection and more. | +| `ImagePreviewActivity` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | This gallery activity has been deprecated in favour of `MediaGalleryPreviewContract`. The new gallery holds multiple improvements such as the ability to reproduce mixed image and video content, automatic reloading upon regaining network connection and more. | | `StreamDimens` constructor containing parameter `attachmentsContentImageHeight` | 2022.08.16
5.8.0 | 2022.08.30
5.9.0 | 2022.09.13 ⌛ | This constructor has been deprecated. Use the constructor that does not contain the parameter `attachmentsContentImageHeight`. | | `QueryChannelsState.chatEventHandler` | 2022.08.16
5.8.0 | 2022.08.30
5.9.0 | 2022.09.13 ⌛ | Use `QueryChannelsState.chatEventHandlerFactory` instead. | | Multiple event specific `BaseChatEventHandler` methods | 2022.08.16
5.8.0 | 2022.08.30
5.9.0 | 2022.09.13 ⌛ | Use `handleChatEvent()` or `handleCidEvent()` instead. | diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/ImageAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/ImageAttachmentContent.kt index 1f62234bf86..52be5c57b81 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/ImageAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/ImageAttachmentContent.kt @@ -63,6 +63,16 @@ import io.getstream.chat.android.uiutils.extension.hasLink */ @OptIn(ExperimentalFoundationApi::class) @Composable +@Deprecated( + message = "Deprecated in favor of 'MediaAttachmentContent'. The new function " + + "is able to preview videos as well as images and has access to the new and improved" + + "media gallery.", + replaceWith = ReplaceWith( + expression = "MediaAttachmentContent()", + "io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentContent", + ), + level = DeprecationLevel.WARNING +) public fun ImageAttachmentContent( attachmentState: AttachmentState, modifier: Modifier = Modifier, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/ImageAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/ImageAttachmentFactory.kt index 38465825815..865e539851c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/ImageAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/ImageAttachmentFactory.kt @@ -30,6 +30,15 @@ import io.getstream.chat.android.compose.ui.util.isMedia * An [AttachmentFactory] that validates attachments as images and uses [ImageAttachmentContent] to * build the UI for the message. */ +@Deprecated( + message = "Deprecated in favor of `MediaAttachmentFactory`. The new factory is able to" + + "preview video content as well as images and has access to the new and improved media gallery.", + replaceWith = ReplaceWith( + expression = "MediaAttachmentFactory()", + "io.getstream.chat.android.compose.ui.attachments.factory.MediaAttachmentFactory" + ), + level = DeprecationLevel.WARNING +) @Suppress("FunctionName") public fun ImageAttachmentFactory(): AttachmentFactory = AttachmentFactory( canHandle = { attachments -> attachments.all { it.isMedia() } }, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/ImagePreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/ImagePreviewActivity.kt index 6f5bff3fbc2..7fbb00c29c7 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/ImagePreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/ImagePreviewActivity.kt @@ -124,6 +124,16 @@ import kotlin.math.abs * Shows an image preview, where we can page through image items, zoom in and perform various actions. */ @OptIn(ExperimentalPagerApi::class) +@Deprecated( + message = "Deprecated in favour of 'MediaGalleryPreviewActivity'. The new activity is able to" + + "reproduce video content as well as images and features a number of improvements such as " + + "automatic reloading upon regaining network connection and more.", + replaceWith = ReplaceWith( + expression = "MediaGalleryPreviewActivity", + "io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewActivity" + ), + level = DeprecationLevel.WARNING +) public class ImagePreviewActivity : AppCompatActivity() { /** @@ -950,6 +960,16 @@ public class ImagePreviewActivity : AppCompatActivity() { * @param messageId The ID of the message to explore the images of. * @param attachmentPosition The initial position of the clicked image. */ + @Deprecated( + message = "Deprecated in favour of 'MediaGalleryPreviewActivity.getIntent'. The new activity is able to" + + "reproduce video content as well as images and features a number of improvements such as " + + "automatic reloading upon regaining network connection and more.", + replaceWith = ReplaceWith( + expression = "MediaGalleryPreviewActivity.getIntent()", + "io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewActivity.getIntent" + ), + level = DeprecationLevel.WARNING + ) public fun getIntent(context: Context, messageId: String, attachmentPosition: Int): Intent { return Intent(context, ImagePreviewActivity::class.java).apply { putExtra(KeyMessageId, messageId) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/ImagePreviewContract.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/ImagePreviewContract.kt index ee31c3c5395..e65e9860565 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/ImagePreviewContract.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/ImagePreviewContract.kt @@ -24,6 +24,17 @@ import io.getstream.chat.android.compose.state.imagepreview.ImagePreviewResult /** * The contract used to start the [ImagePreviewActivity] given a message ID and the position of the clicked attachment. */ +@Deprecated( + message = "Deprecated in favor of 'MediaGalleryPreviewActivity', please use it in combination with" + + "'MediaGalleryPreviewActivity'. The new activity is able to " + + "reproduce video content as well as images and features a number of improvements such as " + + "automatic reloading upon regaining network connection and more.", + replaceWith = ReplaceWith( + expression = "MediaGalleryPreviewContract()", + "io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewContract" + ), + level = DeprecationLevel.WARNING +) public class ImagePreviewContract : ActivityResultContract() { /** @@ -55,6 +66,17 @@ public class ImagePreviewContract : ActivityResultContract Date: Mon, 5 Sep 2022 16:32:19 +0200 Subject: [PATCH 41/69] [3369] Generate detekt baseline --- stream-chat-android-compose/detekt-baseline.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index 55e8d152405..e34fa83ae44 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -11,10 +11,11 @@ ComplexMethod:MessageOptions.kt$@Composable public fun defaultMessageOptionsState( selectedMessage: Message, currentUser: User?, isInThread: Boolean, ownCapabilities: Set<String>, ): List<MessageOptionItemState> ForbiddenComment:MessageText.kt$// TODO: Fix emoji font padding once this is resolved and exposed: https://issuetracker.google.com/issues/171394808 ForbiddenComment:QuotedMessageText.kt$// TODO: Fix emoji font padding once this is resolved and exposed: https://issuetracker.google.com/issues/171394808 + LargeClass:ImagePreviewActivity.kt$ImagePreviewActivity : AppCompatActivity LargeClass:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity : AppCompatActivity LongMethod:GiphyMessageContent.kt$@Composable public fun GiphyMessageContent( message: Message, modifier: Modifier = Modifier, onGiphyActionClick: (GiphyAction) -> Unit = {}, ) LongMethod:GroupAvatar.kt$@Composable public fun GroupAvatar( users: List<User>, modifier: Modifier = Modifier, shape: Shape = ChatTheme.shapes.avatar, textStyle: TextStyle = ChatTheme.typography.captionBold, onClick: (() -> Unit)? = null, ) - LongMethod:ImageAttachmentContent.kt$@OptIn(ExperimentalFoundationApi::class) @Composable public fun ImageAttachmentContent( attachmentState: AttachmentState, modifier: Modifier = Modifier, ) + LongMethod:ImageAttachmentContent.kt$@OptIn(ExperimentalFoundationApi::class) @Composable @Deprecated( message = "Deprecated in favor of 'MediaAttachmentContent'. The new function " + "is able to preview videos as well as images and has access to the new and improved" + "media gallery.", replaceWith = ReplaceWith( expression = "MediaAttachmentContent()", "io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentContent", ), level = DeprecationLevel.WARNING ) public fun ImageAttachmentContent( attachmentState: AttachmentState, modifier: Modifier = Modifier, ) LongMethod:MediaAttachmentContent.kt$@Suppress("LongParameterList") @OptIn(ExperimentalFoundationApi::class) @Composable internal fun MediaAttachmentContentItem( message: Message, attachmentPosition: Int, attachment: Attachment, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit, onLongItemClick: (Message) -> Unit, modifier: Modifier = Modifier, playButton: @Composable () -> Unit, ) LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun ImagePreviewContent( attachment: Attachment, pagerState: PagerState, page: Int, ) LongMethod:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$@Composable private fun MediaGalleryItem( index: Int, attachment: Attachment, user: User, pagerState: PagerState, ) From 5d05023afb811b88b9c9b4d5ee6a872fc3f2823c Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 5 Sep 2022 18:16:59 +0200 Subject: [PATCH 42/69] [3369] Update StreamDimens deprecations --- DEPRECATIONS.md | 1 + .../compose/sample/ui/MessagesActivity.kt | 5 +- .../api/stream-chat-android-compose.api | 13 +- .../content/MediaAttachmentContent.kt | 16 +- .../factory/MediaAttachmentFactory.kt | 5 +- .../android/compose/ui/theme/StreamDimens.kt | 198 ++++++++++++++++-- 6 files changed, 209 insertions(+), 29 deletions(-) diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index fac70e325e4..4f9f639172c 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -4,6 +4,7 @@ This document lists deprecated constructs in the SDK, with their expected time | API / Feature | Deprecated (warning) | Deprecated (error) | Removed | Notes | | --- | --- | --- | --- | --- | +| `StreamDimens` constructor containing parameter `attachmentsContentImageGridSpacing` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | This constructor has been deprecated. Use the constructor that does not contain the parameter `attachmentsContentImageGridSpacing`. | | `ImageAttachmentContent` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | `ImageAttachmentContent` has been deprecated in favor of `MediattachmentContent`. The new function is able to preview videos as well as images and has access to a new and improved media gallery. | | `ImageAttachmentFactory` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | `ImageAttachmentFactory` has been deprecated in favor of `MediaAttachmentFactory`. The new factory is able to preview videos as well as images and has access to a new and improved media gallery. | | `ImagePreviewContract` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | `ImagePreviewContract` has been deprecated in favor of `MediaGalleryPreviewContract`, please use it in conjunction with `MediaGalleryPreviewActivity`. The new gallery holds multiple improvements such as the ability to reproduce mixed image and video content, automatic reloading upon regaining network connection and more. | diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt index 218b5c603d5..6d65edd2d6e 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt @@ -93,10 +93,7 @@ class MessagesActivity : BaseConnectedActivity() { val channelId = intent.getStringExtra(KEY_CHANNEL_ID) ?: return setContent { - ChatTheme( - dateFormatter = ChatApp.dateFormatter, - videoThumbnailsEnabled = false - ) { + ChatTheme(dateFormatter = ChatApp.dateFormatter) { MessagesScreen( channelId = channelId, onBackPressed = { finish() }, diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index b0c90d26789..081906d3f75 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1655,8 +1655,9 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors$Compa public final class io/getstream/chat/android/compose/ui/theme/StreamDimens { public static final field Companion Lio/getstream/chat/android/compose/ui/theme/StreamDimens$Companion; + public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-D9Ej5fM ()F public final fun component10-D9Ej5fM ()F public final fun component11-D9Ej5fM ()F @@ -1699,26 +1700,30 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamDimens { public final fun component45-D9Ej5fM ()F public final fun component46-D9Ej5fM ()F public final fun component47-D9Ej5fM ()F + public final fun component48-D9Ej5fM ()F + public final fun component49-D9Ej5fM ()F public final fun component5-D9Ej5fM ()F public final fun component6-D9Ej5fM ()F public final fun component7-D9Ej5fM ()F public final fun component8-D9Ej5fM ()F public final fun component9-D9Ej5fM ()F - public final fun copy-TVlVuw8 (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; - public static synthetic fun copy-TVlVuw8$default (Lio/getstream/chat/android/compose/ui/theme/StreamDimens;FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; + public final fun copy-IGK3VHk (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; + public static synthetic fun copy-IGK3VHk$default (Lio/getstream/chat/android/compose/ui/theme/StreamDimens;FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; public fun equals (Ljava/lang/Object;)Z public final fun getAttachmentsContentFileUploadWidth-D9Ej5fM ()F public final fun getAttachmentsContentFileWidth-D9Ej5fM ()F public final fun getAttachmentsContentGiphyHeight-D9Ej5fM ()F public final fun getAttachmentsContentGiphyWidth-D9Ej5fM ()F + public final fun getAttachmentsContentGroupPreviewHeight-D9Ej5fM ()F + public final fun getAttachmentsContentGroupPreviewWidth-D9Ej5fM ()F public final fun getAttachmentsContentImageGridSpacing-D9Ej5fM ()F public final fun getAttachmentsContentImageHeight-D9Ej5fM ()F public final fun getAttachmentsContentImageMaxHeight-D9Ej5fM ()F public final fun getAttachmentsContentImageWidth-D9Ej5fM ()F public final fun getAttachmentsContentLinkWidth-D9Ej5fM ()F public final fun getAttachmentsContentMediaGridSpacing-D9Ej5fM ()F - public final fun getAttachmentsContentMediaWidth-D9Ej5fM ()F public final fun getAttachmentsContentVideoMaxHeight-D9Ej5fM ()F + public final fun getAttachmentsContentVideoWidth-D9Ej5fM ()F public final fun getChannelAvatarSize-D9Ej5fM ()F public final fun getChannelItemHorizontalPadding-D9Ej5fM ()F public final fun getChannelItemVerticalPadding-D9Ej5fM ()F diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 86ca566bcc0..95803bf9887 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -30,7 +30,9 @@ import androidx.compose.foundation.layout.RowScope 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.heightIn +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.Text import androidx.compose.material.ripple.rememberRipple @@ -176,7 +178,13 @@ internal fun ShowSingleMediaAttachment( ChatTheme.dimens.attachmentsContentImageMaxHeight } ) - .fillMaxWidth() + .width( + if (attachment.type == AttachmentType.VIDEO) { + ChatTheme.dimens.attachmentsContentVideoWidth + } else { + ChatTheme.dimens.attachmentsContentImageWidth + } + ) .aspectRatio(ratio ?: EqualDimensionsRatio), message = message, attachmentPosition = 0, @@ -217,7 +225,8 @@ internal fun RowScope.ShowMultipleMediaAttachments( Column( modifier = Modifier .weight(1f, fill = false) - .aspectRatio(TwiceAsTallAsIsWideRatio), + .width(ChatTheme.dimens.attachmentsContentGroupPreviewWidth / 2) + .height(ChatTheme.dimens.attachmentsContentGroupPreviewHeight), verticalArrangement = Arrangement.spacedBy(gridSpacing) ) { for (attachmentIndex in 0 until maximumNumberOfPreviewedItems step 2) { @@ -238,7 +247,8 @@ internal fun RowScope.ShowMultipleMediaAttachments( Column( modifier = Modifier .weight(1f, fill = false) - .aspectRatio(TwiceAsTallAsIsWideRatio), + .width(ChatTheme.dimens.attachmentsContentGroupPreviewWidth / 2) + .height(ChatTheme.dimens.attachmentsContentGroupPreviewHeight), verticalArrangement = Arrangement.spacedBy(gridSpacing) ) { for (attachmentIndex in 1 until maximumNumberOfPreviewedItems step 2) { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt index 29bfd3c2111..e057c3df7bb 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -32,7 +31,6 @@ import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentContent import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentPreviewContent import io.getstream.chat.android.compose.ui.attachments.content.PlayButton -import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.uiutils.constant.AttachmentType /** @@ -71,8 +69,7 @@ public fun MediaAttachmentFactory( }, content = @Composable { modifier, state -> MediaAttachmentContent( - modifier = modifier - .width(ChatTheme.dimens.attachmentsContentMediaWidth), + modifier = modifier, attachmentState = state, maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, playButton = { contentPlayButton() } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt index 8db2d3d0536..88a25aae83c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt @@ -73,14 +73,20 @@ import androidx.compose.ui.unit.dp * @param attachmentsContentVideoMaxHeight The maximum height video attachment will expand to while automatically * re-sizing itself in order to obey its aspect ratio. * @param attachmentsContentMediaGridSpacing The spacing between media preview tiles in the message list. - * @param attachmentsContentMediaWidth The width of media attachment previews in the message list. + * @param attachmentsContentVideoWidth The width of media attachment previews in the message list. + * @param attachmentsContentGroupPreviewWidth The width of the container displaying media previews tiled in + * a group in the message list. + * @param attachmentsContentGroupPreviewHeight The height of the container displaying media previews tiled in + * a group in the message list. */ @Immutable public data class StreamDimens @Deprecated( - "This constructor has been deprecated. Parameter 'attachmentsContentImageHeight' has been deprecated in" + - " favor of 'attachmentsContentImageMaxHeight'. Please use the constructor which does not contain " + - "`attachmentsContentImageHeight`", + "This constructor has been deprecated. Parameters 'attachmentsContentImageHeight' and " + + "'attachmentsContentImageGridSpacing' have been deprecated in" + + " favor of 'attachmentsContentImageMaxHeight' and 'attachmentsContentMediaGridSpacing'. " + + "Please use the constructor which does not contain `attachmentsContentImageHeight` and " + + "attachmentsContentImageGridSpacing.", level = DeprecationLevel.ERROR, ) constructor( @@ -90,9 +96,7 @@ constructor( public val selectedChannelMenuUserItemWidth: Dp, public val selectedChannelMenuUserItemHorizontalPadding: Dp, public val selectedChannelMenuUserItemAvatarSize: Dp, - // TODO - deprecate in favor of attachmentsContentMediaWidth public val attachmentsContentImageWidth: Dp, - // TODO - deprecate in favor of attachmentsContentMediaGridSpacing public val attachmentsContentImageGridSpacing: Dp, public val attachmentsContentImageHeight: Dp, public val attachmentsContentGiphyWidth: Dp, @@ -132,7 +136,9 @@ constructor( public val attachmentsContentImageMaxHeight: Dp, public val attachmentsContentVideoMaxHeight: Dp, public val attachmentsContentMediaGridSpacing: Dp, - public val attachmentsContentMediaWidth: Dp, + public val attachmentsContentVideoWidth: Dp, + public val attachmentsContentGroupPreviewWidth: Dp, + public val attachmentsContentGroupPreviewHeight: Dp, ) { /** @@ -188,9 +194,13 @@ constructor( * re-sizing itself in order to obey its aspect ratio. * @param attachmentsContentVideoMaxHeight The maximum height video attachment will expand to while automatically * re-sizing itself in order to obey its aspect ratio. - * @param attachmentsContentMediaGridSpacing The spacing between media preview tiles in the message list. - * @param attachmentsContentMediaWidth The width of media attachment previews in the message list. */ + @Deprecated( + "This constructor has been deprecated. Parameter 'attachmentsContentImageGridSpacing' has been deprecated in" + + " favor of 'attachmentsContentMediaGridSpacing'. Please use the constructor which does not contain " + + "`attachmentsContentImageGridSpacing`", + level = DeprecationLevel.ERROR, + ) public constructor( channelItemVerticalPadding: Dp, channelItemHorizontalPadding: Dp, @@ -236,8 +246,6 @@ constructor( groupAvatarInitialsYOffset: Dp, attachmentsContentImageMaxHeight: Dp, attachmentsContentVideoMaxHeight: Dp, - attachmentsContentMediaGridSpacing: Dp, - attachmentsContentMediaWidth: Dp, ) : this( channelItemVerticalPadding = channelItemVerticalPadding, channelItemHorizontalPadding = channelItemHorizontalPadding, @@ -284,8 +292,169 @@ constructor( groupAvatarInitialsYOffset = groupAvatarInitialsYOffset, attachmentsContentImageMaxHeight = attachmentsContentImageMaxHeight, attachmentsContentVideoMaxHeight = attachmentsContentVideoMaxHeight, + attachmentsContentMediaGridSpacing = 2.dp, + attachmentsContentVideoWidth = 400.dp, + attachmentsContentGroupPreviewWidth = 250.dp, + attachmentsContentGroupPreviewHeight = 250.dp + ) + + /** + * Contains all the dimens we provide for our components. + * + * @param channelItemVerticalPadding The vertical content padding inside channel list item. + * @param channelItemHorizontalPadding The horizontal content padding inside channel list item. + * @param channelAvatarSize The size of channel avatar. + * @param selectedChannelMenuUserItemWidth The width of a member tile in the selected channel menu. + * @param selectedChannelMenuUserItemHorizontalPadding The padding inside a member tile in the selected channel + * menu. + * @param selectedChannelMenuUserItemAvatarSize The size of a member avatar in the selected channel menu. + * @param attachmentsContentImageWidth The width of image attachments in the message list. + * @param attachmentsContentGiphyWidth The with of Giphy attachments in the message list. + * @param attachmentsContentGiphyHeight The height of Giphy attachments in the message list. + * @param attachmentsContentLinkWidth The with of link attachments in the message list. + * @param attachmentsContentFileWidth The width of file attachments in the message list. + * @param attachmentsContentFileUploadWidth The width of uploading file attachments in the message list. + * @param threadSeparatorVerticalPadding The vertical content padding inside thread separator item. + * @param threadSeparatorTextVerticalPadding The vertical padding inside thread separator text. + * @param messageOptionsItemHeight The height of a message option item. + * @param suggestionListMaxHeight The maximum height of the suggestion list popup. + * @param suggestionListPadding The outer padding of the suggestion list popup. + * @param suggestionListElevation THe elevation of the suggestion list popup. + * @param mentionSuggestionItemHorizontalPadding The horizontal content padding inside mention list item. + * @param mentionSuggestionItemVerticalPadding The vertical content padding inside mention list item. + * @param mentionSuggestionItemAvatarSize The size of a channel avatar in the suggestion list popup. + * @param commandSuggestionItemHorizontalPadding The horizontal content padding inside command list item. + * @param commandSuggestionItemVerticalPadding The vertical content padding inside command list item. + * @param commandSuggestionItemIconSize The size of a command icon in the suggestion list popup. + * @param threadParticipantItemSize The size of thread participant avatar items. + * @param userReactionsMaxHeight The max height of the message reactions section when we click on message reactions. + * @param userReactionItemWidth The width of user reaction item. + * @param userReactionItemAvatarSize The size of a user avatar in the user reaction item. + * @param userReactionItemIconSize The size of a reaction icon in the user reaction item. + * @param reactionOptionItemIconSize The size of a reaction option icon in the reaction options menu. + * @param headerElevation The elevation of the headers, such as the ones appearing on the Channel or Message + * screens. + * @param messageItemMaxWidth The max width of message items inside message list. + * @param quotedMessageTextVerticalPadding The vertical padding of text inside quoted message. + * @param quotedMessageTextHorizontalPadding The horizontal padding of text inside quoted message. + * @param quotedMessageAttachmentPreviewSize The size of the quoted message attachment preview. + * @param quotedMessageAttachmentTopPadding The top padding of the quoted message attachment preview. + * @param quotedMessageAttachmentBottomPadding The bottom padding of the quoted message attachment preview. + * @param quotedMessageAttachmentStartPadding The start padding of the quoted message attachment preview. + * @param quotedMessageAttachmentEndPadding The end padding of the quoted message attachment preview. + * @param groupAvatarInitialsXOffset The x offset of the user initials inside avatar when there are more than two + * users. + * @param groupAvatarInitialsYOffset The y offset of the user initials inside avatar when there are more than two + * users. + * @param attachmentsContentImageMaxHeight The maximum height an image attachment will expand to while automatically + * re-sizing itself in order to obey its aspect ratio. + * @param attachmentsContentVideoMaxHeight The maximum height video attachment will expand to while automatically + * re-sizing itself in order to obey its aspect ratio. + * @param attachmentsContentMediaGridSpacing The spacing between media preview tiles in the message list. + * @param attachmentsContentVideoWidth The width of media attachment previews in the message list. + * @param attachmentsContentGroupPreviewWidth The width of the container displaying media previews tiled in + * a group in the message list. + * @param attachmentsContentGroupPreviewHeight The height of the container displaying media previews tiled in + * a group in the message list. + */ + public constructor( + channelItemVerticalPadding: Dp, + channelItemHorizontalPadding: Dp, + channelAvatarSize: Dp, + selectedChannelMenuUserItemWidth: Dp, + selectedChannelMenuUserItemHorizontalPadding: Dp, + selectedChannelMenuUserItemAvatarSize: Dp, + attachmentsContentImageWidth: Dp, + attachmentsContentGiphyWidth: Dp, + attachmentsContentGiphyHeight: Dp, + attachmentsContentLinkWidth: Dp, + attachmentsContentFileWidth: Dp, + attachmentsContentFileUploadWidth: Dp, + threadSeparatorVerticalPadding: Dp, + threadSeparatorTextVerticalPadding: Dp, + messageOptionsItemHeight: Dp, + suggestionListMaxHeight: Dp, + suggestionListPadding: Dp, + suggestionListElevation: Dp, + mentionSuggestionItemHorizontalPadding: Dp, + mentionSuggestionItemVerticalPadding: Dp, + mentionSuggestionItemAvatarSize: Dp, + commandSuggestionItemHorizontalPadding: Dp, + commandSuggestionItemVerticalPadding: Dp, + commandSuggestionItemIconSize: Dp, + threadParticipantItemSize: Dp, + userReactionsMaxHeight: Dp, + userReactionItemWidth: Dp, + userReactionItemAvatarSize: Dp, + userReactionItemIconSize: Dp, + reactionOptionItemIconSize: Dp, + headerElevation: Dp, + messageItemMaxWidth: Dp, + quotedMessageTextVerticalPadding: Dp, + quotedMessageTextHorizontalPadding: Dp, + quotedMessageAttachmentPreviewSize: Dp, + quotedMessageAttachmentTopPadding: Dp, + quotedMessageAttachmentBottomPadding: Dp, + quotedMessageAttachmentStartPadding: Dp, + quotedMessageAttachmentEndPadding: Dp, + groupAvatarInitialsXOffset: Dp, + groupAvatarInitialsYOffset: Dp, + attachmentsContentImageMaxHeight: Dp, + attachmentsContentVideoMaxHeight: Dp, + attachmentsContentVideoWidth: Dp, + attachmentsContentMediaGridSpacing: Dp, + attachmentsContentGroupPreviewWidth: Dp, + attachmentsContentGroupPreviewHeight: Dp, + ) : this( + channelItemVerticalPadding = channelItemVerticalPadding, + channelItemHorizontalPadding = channelItemHorizontalPadding, + channelAvatarSize = channelAvatarSize, + selectedChannelMenuUserItemWidth = selectedChannelMenuUserItemWidth, + selectedChannelMenuUserItemHorizontalPadding = selectedChannelMenuUserItemHorizontalPadding, + selectedChannelMenuUserItemAvatarSize = selectedChannelMenuUserItemAvatarSize, + attachmentsContentImageWidth = attachmentsContentImageWidth, + attachmentsContentImageGridSpacing = 2.dp, + attachmentsContentImageHeight = 200.dp, + attachmentsContentGiphyWidth = attachmentsContentGiphyWidth, + attachmentsContentGiphyHeight = attachmentsContentGiphyHeight, + attachmentsContentLinkWidth = attachmentsContentLinkWidth, + attachmentsContentFileWidth = attachmentsContentFileWidth, + attachmentsContentFileUploadWidth = attachmentsContentFileUploadWidth, + threadSeparatorVerticalPadding = threadSeparatorVerticalPadding, + threadSeparatorTextVerticalPadding = threadSeparatorTextVerticalPadding, + messageOptionsItemHeight = messageOptionsItemHeight, + suggestionListMaxHeight = suggestionListMaxHeight, + suggestionListPadding = suggestionListPadding, + suggestionListElevation = suggestionListElevation, + mentionSuggestionItemHorizontalPadding = mentionSuggestionItemHorizontalPadding, + mentionSuggestionItemVerticalPadding = mentionSuggestionItemVerticalPadding, + mentionSuggestionItemAvatarSize = mentionSuggestionItemAvatarSize, + commandSuggestionItemHorizontalPadding = commandSuggestionItemHorizontalPadding, + commandSuggestionItemVerticalPadding = commandSuggestionItemVerticalPadding, + commandSuggestionItemIconSize = commandSuggestionItemIconSize, + threadParticipantItemSize = threadParticipantItemSize, + userReactionsMaxHeight = userReactionsMaxHeight, + userReactionItemWidth = userReactionItemWidth, + userReactionItemAvatarSize = userReactionItemAvatarSize, + userReactionItemIconSize = userReactionItemIconSize, + reactionOptionItemIconSize = reactionOptionItemIconSize, + headerElevation = headerElevation, + messageItemMaxWidth = messageItemMaxWidth, + quotedMessageTextVerticalPadding = quotedMessageTextVerticalPadding, + quotedMessageTextHorizontalPadding = quotedMessageTextHorizontalPadding, + quotedMessageAttachmentPreviewSize = quotedMessageAttachmentPreviewSize, + quotedMessageAttachmentTopPadding = quotedMessageAttachmentTopPadding, + quotedMessageAttachmentBottomPadding = quotedMessageAttachmentBottomPadding, + quotedMessageAttachmentStartPadding = quotedMessageAttachmentStartPadding, + quotedMessageAttachmentEndPadding = quotedMessageAttachmentEndPadding, + groupAvatarInitialsXOffset = groupAvatarInitialsXOffset, + groupAvatarInitialsYOffset = groupAvatarInitialsYOffset, + attachmentsContentImageMaxHeight = attachmentsContentImageMaxHeight, + attachmentsContentVideoMaxHeight = attachmentsContentVideoMaxHeight, attachmentsContentMediaGridSpacing = attachmentsContentMediaGridSpacing, - attachmentsContentMediaWidth = attachmentsContentMediaWidth + attachmentsContentVideoWidth = attachmentsContentVideoWidth, + attachmentsContentGroupPreviewWidth = attachmentsContentGroupPreviewWidth, + attachmentsContentGroupPreviewHeight = attachmentsContentGroupPreviewHeight ) public companion object { @@ -302,7 +471,6 @@ constructor( selectedChannelMenuUserItemHorizontalPadding = 8.dp, selectedChannelMenuUserItemAvatarSize = 64.dp, attachmentsContentImageWidth = 250.dp, - attachmentsContentImageGridSpacing = 2.dp, attachmentsContentGiphyWidth = 250.dp, attachmentsContentGiphyHeight = 200.dp, attachmentsContentLinkWidth = 250.dp, @@ -340,7 +508,9 @@ constructor( attachmentsContentImageMaxHeight = 600.dp, attachmentsContentVideoMaxHeight = 400.dp, attachmentsContentMediaGridSpacing = 2.dp, - attachmentsContentMediaWidth = 250.dp + attachmentsContentVideoWidth = 250.dp, + attachmentsContentGroupPreviewWidth = 250.dp, + attachmentsContentGroupPreviewHeight = 250.dp, ) } } From f673ad12e511cd7ff3d7ff4715da50227bbb63c5 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 5 Sep 2022 18:18:36 +0200 Subject: [PATCH 43/69] [3369] Generate detekt baseline. --- stream-chat-android-compose/detekt-baseline.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index e34fa83ae44..15394c271eb 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -54,6 +54,7 @@ MaxLineLength:MessagesViewModelFactory.kt$MessagesViewModelFactory$private val dateSeparatorThresholdMillis: Long = TimeUnit.HOURS.toMillis(MessageListViewModel.DateSeparatorDefaultHourThreshold) ReturnCount:MessageListViewModel.kt$MessageListViewModel$public fun updateLastSeenMessage(message: Message) TooManyFunctions:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity : AppCompatActivity + UnusedPrivateMember:MediaAttachmentContent.kt$/** * Produces a height value that is twice the width of the * Composable when calling [Modifier.aspectRatio]. */ private const val TwiceAsTallAsIsWideRatio = 0.5f UnusedPrivateMember:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$private fun shareVideo(videoUri: Uri) UnusedPrivateMember:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$videoUri: Uri
From 684028d7c327fcfe74aed26e471a31d8e7241f3b Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Mon, 5 Sep 2022 19:01:48 +0200 Subject: [PATCH 44/69] [3369] Update docusaurus --- .../05-message-components/03-message-list.mdx | 15 ++++++++------- .../docs/kotlin/compose/messages/MessageList.kt | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docusaurus/docs/Android/04-compose/05-message-components/03-message-list.mdx b/docusaurus/docs/Android/04-compose/05-message-components/03-message-list.mdx index 8a6f70f7c3b..59767f46baa 100644 --- a/docusaurus/docs/Android/04-compose/05-message-components/03-message-list.mdx +++ b/docusaurus/docs/Android/04-compose/05-message-components/03-message-list.mdx @@ -85,8 +85,8 @@ fun MessageList( onLastVisibleMessageChanged: (Message) -> Unit = { viewModel.updateLastSeenMessage(it) }, onScrollToBottom: () -> Unit = { viewModel.clearNewMessageState() }, onGiphyActionClick: (GiphyAction) -> Unit = { viewModel.performGiphyAction(it) }, - onImagePreviewResult: (ImagePreviewResult?) -> Unit = { - if (it?.resultType == ImagePreviewResultType.SHOW_IN_CHAT) { + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = { + if (it?.resultType == MediaGalleryPreviewResultType.SHOW_IN_CHAT) { viewModel.focusMessage(it.messageId) } }, @@ -101,7 +101,7 @@ fun MessageList( * `onLastVisibleMessageChanged`: Handler used when the user scrolls and the last visible item changes. * `onScrollToBottom`: Handler used when the user reaches the newest message. Used to remove the "New message" or "Scroll to bottom" actions from the UI. * `onGiphyActionClick`: Handler used when the user clicks on one of the actions in a Giphy message. Giphy images with actions are displayed only directly after using the Giphy slash command. -* `onImagePreviewResult`: Handler used when the user receives a result from the Image Preview screen, after opening an image attachment. +* `onMediaGalleryPreviewResult`: Handler used when the user receives a result from the Media Gallery Preview screen, after opening an image or a video attachment. You can customize the behavior here by providing your own actions, like so: @@ -116,8 +116,8 @@ MessageList( onLastVisibleMessageChanged = { message -> }, onScrollToBottom = { }, onGiphyActionClick = { giphyAction -> }, - onImagePreviewResult = { imagePreviewResult -> }, - // Content + onMediaGalleryPreviewResult = { mediaGalleryPreviewResult -> }, + // Content ) ``` @@ -176,11 +176,12 @@ fun MessageList( itemContent: @Composable (MessageListItemState) -> Unit = { messageListItem -> DefaultMessageContainer( messageListItem = messageListItem, - onImagePreviewResult = onImagePreviewResult, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onThreadClick = onThreadClick, onLongItemClick = onLongItemClick, onReactionsClick = onReactionsClick, - onGiphyActionClick = onGiphyActionClick + onGiphyActionClick = onGiphyActionClick, + onQuotedMessageClick = onQuotedMessageClick, ) }, ) diff --git a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/MessageList.kt b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/MessageList.kt index 8ab2818ef3b..67626a75cd6 100644 --- a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/MessageList.kt +++ b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/messages/MessageList.kt @@ -97,7 +97,7 @@ private object MessageListHandlingActionsSnippet { onLastVisibleMessageChanged = { message -> }, onScrollToBottom = { }, onGiphyActionClick = { giphyAction -> }, - onImagePreviewResult = { imagePreviewResult -> }, + onMediaGalleryPreviewResult = { mediaGalleryPreviewResult -> }, ) } } From 9d9e0e7fa0d0839976afeda84050c8e744b2512a Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 6 Sep 2022 11:14:41 +0200 Subject: [PATCH 45/69] [3369] Update the changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc50b03306f..f1b5bcfad48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,8 +71,14 @@ ### 🐞 Fixed ### ⬆️ Improved +- Improved automatic reloading of non-cached images when regaining network connection. The improvements are visible in the messages list and the new media gallery called `MediaGalleryPreviewActivity`. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) ### ✅ Added +- Added a new gallery called `MediaGalleryPreviewActivity`. This gallery is an upgrade over `ImagePreviewActivity` as it has the capability to reproduce videos as well as images, automatically reloads non-cached images upon regaining network connection and works in offline mode. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) +- Added `MediaAttachmentContent`. The new composable is an improvement over `ImageAttachmentContent` as it has the ability to preview both videos and images and has access to the new and improved media gallery and the ability to tile more than 4 previews by modifying the parameter `maximumNumberOfPreviewedItems`. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) +- Added `MediaAttachmentFactory`. The new factory is an improvement over `ImageAttachmentFactory`. The new factory hs the ability to preview videos and the ability to tile more than 4 previews in a group by changing the value of the parameter `maximumNumberOfPreviewedItems`. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) +- Added parameters `attachmentsContentVideoMaxHeight`, `attachmentsContentMediaGridSpacing`, `attachmentsContentVideoWidth`, `attachmentsContentGroupPreviewWidth` and `attachmentsContentGroupPreviewHeight` to `StreamDimens`. These parameters are meant for more finer grained control over how media previews are displayed in the message list. For the best aesthetic outcome, the width of these should be equal to the value in `StreamDimens.messageItemMaxWidth`. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) +- Added the ability to turn off video previews (thumbnails) via `ChatTheme.videoThumbnailsEnabled`. Video previews are a paid feature and as such you can turn them off. They are on by default and the pricing can be found [here](https://getstream.io/chat/pricing/). [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) ### ⚠️ Changed - 🚨 Breaking change: `MessageAttachmentsContent` function parameter `onImagePreviewResult: (ImagePreviewResult?) -> Unit` has been replaced with `onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit`. Functionally `ImagePreviewResult` and `MediaGalleryPreviewResult` are the same, the only difference is the activity they are returned from so changes should be minimal. @@ -80,6 +86,10 @@ - 🚨 Breaking change: `MessageContent` function parameter `onImagePreviewResult: (ImagePreviewResult?) -> Unit` has been replaced with `onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit`. Functionally `ImagePreviewResult` and `MediaGalleryPreviewResult` are the same, the only difference is the activity they are returned from so changes should be minimal. - 🚨 Breaking change: `MessageContainer` function parameter `onImagePreviewResult: (ImagePreviewResult?) -> Unit` has been replaced with `onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit`. Functionally `ImagePreviewResult` and `MediaGalleryPreviewResult` are the same, the only difference is the activity they are returned from so changes should be minimal. - 🚨 Breaking change: Both bound (with `MessageListViewModel` as a parameter) and unbound `MessageList` Composable functions have had parameter `onImagePreviewResult: (ImagePreviewResult?) -> Unit` replaced with `onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit`. Functionally `ImagePreviewResult` and `MediaGalleryPreviewResult` are the same, the only difference is the activity they are returned from so changes should be minimal. +- Video previews are now automatically displayed. These are a paid feature and can be turned off via `ChatTheme.videoThumbnailsEnabled`. If you are interested in the pricing before making a decision, you can find it [here](https://getstream.io/chat/pricing/). [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) +- Started the deprecation process for `ImagePreviewActivity`, please use `MediaGalleryPreviewActivity` as it has all the functionality of the previous gallery while adding additional features such as video playback and offline capabilities. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) +- Started the deprecation process for `ImageAttachmentFactory`, please use `MediaAttachmentFactory` as it has all the functionality of the previous factory while adding additional features such as displaying video previews modifiable number of tiles in a group preview. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) +- Started the deprecation process for `ImageAttachmentContent`, please use `MediaAttachmentContent` as it has all the functionality of the previous component while adding additional features such as displaying video previous and modifiable number of tiles in a group preview. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) ### ❌ Removed @@ -87,6 +97,7 @@ ### 🐞 Fixed ### ⬆️ Improved +- The default factory for previewing video and image attachment now is `MediaAttachmentFactory`. It holds numerous improvements, the biggest of which are the ability to reload the image intelligently if the image wasn't loaded and network connection is re-established and the access to the new and improved media gallery. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) ### ✅ Added From 8362bade98838d7511aababb5bda208a33a78c16 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 6 Sep 2022 11:35:35 +0200 Subject: [PATCH 46/69] [3369] Add video thumbnails to file attachment content as well --- .../content/FileAttachmentContent.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContent.kt index 65e5407af02..32f02b7b7e9 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContent.kt @@ -52,6 +52,7 @@ import io.getstream.chat.android.compose.state.messages.attachments.AttachmentSt import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.MimeTypeIconProvider import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter +import io.getstream.chat.android.uiutils.constant.AttachmentType /** * Builds a file attachment message which shows a list of files. @@ -197,17 +198,24 @@ private fun RowScope.FileAttachmentDownloadIcon(attachment: Attachment) { */ @Composable public fun FileAttachmentImage(attachment: Attachment) { - val isImage = attachment.type == "image" + val isImage = attachment.type == AttachmentType.IMAGE + val isVideoWithThumbnails = attachment.type == AttachmentType.VIDEO && ChatTheme.videoThumbnailsEnabled - val painter = if (isImage) { - val dataToLoad = attachment.imageUrl ?: attachment.upload + val painter = when { + isImage -> { + val dataToLoad = attachment.imageUrl ?: attachment.upload - rememberStreamImagePainter(dataToLoad) - } else { - painterResource(id = MimeTypeIconProvider.getIconRes(attachment.mimeType)) + rememberStreamImagePainter(dataToLoad) + } + isVideoWithThumbnails -> { + val dataToLoad = attachment.thumbUrl ?: attachment.upload + + rememberStreamImagePainter(dataToLoad) + } + else -> painterResource(id = MimeTypeIconProvider.getIconRes(attachment.mimeType)) } - val shape = if (isImage) ChatTheme.shapes.imageThumbnail else null + val shape = if (isImage || isVideoWithThumbnails) ChatTheme.shapes.imageThumbnail else null val imageModifier = Modifier.size(height = 40.dp, width = 35.dp).let { baseModifier -> if (shape != null) baseModifier.clip(shape) else baseModifier @@ -217,6 +225,6 @@ public fun FileAttachmentImage(attachment: Attachment) { modifier = imageModifier, painter = painter, contentDescription = null, - contentScale = if (isImage) ContentScale.Crop else ContentScale.Fit + contentScale = if (isImage || isVideoWithThumbnails) ContentScale.Crop else ContentScale.Fit ) } From 611c84bcf2b5571f32dce54102cc3f98e156bc8a Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 6 Sep 2022 11:52:10 +0200 Subject: [PATCH 47/69] [3369] Update bad deprecations merge conflict resolution --- DEPRECATIONS.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 4f9f639172c..9ab198355d2 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -9,16 +9,12 @@ This document lists deprecated constructs in the SDK, with their expected time | `ImageAttachmentFactory` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | `ImageAttachmentFactory` has been deprecated in favor of `MediaAttachmentFactory`. The new factory is able to preview videos as well as images and has access to a new and improved media gallery. | | `ImagePreviewContract` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | `ImagePreviewContract` has been deprecated in favor of `MediaGalleryPreviewContract`, please use it in conjunction with `MediaGalleryPreviewActivity`. The new gallery holds multiple improvements such as the ability to reproduce mixed image and video content, automatic reloading upon regaining network connection and more. | | `ImagePreviewActivity` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | This gallery activity has been deprecated in favour of `MediaGalleryPreviewContract`. The new gallery holds multiple improvements such as the ability to reproduce mixed image and video content, automatic reloading upon regaining network connection and more. | +| Lambda parameter `AttachmentState.onImagePreviewResult` | 2022.09.13
5.8.2 | 2022.10.01 ⌛ | 2022.10.15 ⌛ | Replace it with lambda parameter `AttachmentState.onMediaGalleryPreviewResult` | +| `AttachmentState` constructor containing parameter `onImagePreviewResult` | 2022.09.17
5.8.2 | 2022.10.01 ⌛ | 2022.10.15 ⌛ | This constructor has been deprecated. Use the constructor that does not contain the parameter `onImagePreviewResult`. | | `StreamDimens` constructor containing parameter `attachmentsContentImageHeight` | 2022.08.16
5.8.0 | 2022.08.30
5.9.0 | 2022.09.13 ⌛ | This constructor has been deprecated. Use the constructor that does not contain the parameter `attachmentsContentImageHeight`. | | `QueryChannelsState.chatEventHandler` | 2022.08.16
5.8.0 | 2022.08.30
5.9.0 | 2022.09.13 ⌛ | Use `QueryChannelsState.chatEventHandlerFactory` instead. | | Multiple event specific `BaseChatEventHandler` methods | 2022.08.16
5.8.0 | 2022.08.30
5.9.0 | 2022.09.13 ⌛ | Use `handleChatEvent()` or `handleCidEvent()` instead. | | `NonMemberChatEventHandler` | 2022.08.16
5.8.0 | 2022.08.30
5.9.0 | 2022.09.13 ⌛ | Use `BaseChatEventHandler` or `DefaultChatEventHandler` instead. | -| Lambda parameter `AttachmentState.onImagePreviewResult` | 2022.09.17
5.8.2 | 2022.10.01 ⌛ | 2022.10.15 ⌛ | Replace it with lambda parameter `AttachmentState.onMediaGalleryPreviewResult` | -| `AttachmentState` constructor containing parameter `onImagePreviewResult` | 2022.09.17
5.8.2 | 2022.10.01 ⌛ | 2022.10.15 ⌛ | This constructor has been deprecated. Use the constructor that does not contain the parameter `onImagePreviewResult`. | -| `StreamDimens` constructor containing parameter `attachmentsContentImageHeight` | 2022.08.16
5.8.0 | 2022.08.30 ⌛ | 2022.09.13 ⌛ | This constructor has been deprecated. Use the constructor that does not contain the parameter `attachmentsContentImageHeight`. | -| `QueryChannelsState.chatEventHandler` | 2022.08.16
5.8.0 | 2022.08.30 ⌛ | 2022.09.13 ⌛ | Use `QueryChannelsState.chatEventHandlerFactory` instead. | -| Multiple event specific `BaseChatEventHandler` methods | 2022.08.16
5.8.0 | 2022.08.30 ⌛ | 2022.09.13 ⌛ | Use `handleChatEvent()` or `handleCidEvent()` instead. | -| `NonMemberChatEventHandler` | 2022.08.16
5.8.0 | 2022.08.30 ⌛ | 2022.09.13 ⌛ | Use `BaseChatEventHandler` or `DefaultChatEventHandler` instead. | | `ClientState.initialized` | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | Use ClientState.initializationState instead. | | `MessageListViewModel.BlockUser` | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | Deprecated in order to make the action more explicit. Use `MessageListViewModel.ShadowBanUser` if you want to retain the same functionality, or `MessageListViewModel.BanUser` if you want to outright ban the user. The difference between banning and shadow banning can be found here: https://getstream.io/blog/feature-announcement-shadow-ban/ | | `MessageAction.MuteUser` | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | The option to mute users via a message option has been deprecated and will be removed. | From c4dd372a534426fabd0bae4333cd655e92c290f0 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 6 Sep 2022 12:31:15 +0200 Subject: [PATCH 48/69] [3369] Update deprecations --- CHANGELOG.md | 1 + DEPRECATIONS.md | 1 + .../attachments/content/ImageAttachmentQuotedContent.kt | 9 +++++++++ .../ui/attachments/factory/QuotedAttachmentFactory.kt | 2 +- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1b5bcfad48..151c01b00f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ - Started the deprecation process for `ImagePreviewActivity`, please use `MediaGalleryPreviewActivity` as it has all the functionality of the previous gallery while adding additional features such as video playback and offline capabilities. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) - Started the deprecation process for `ImageAttachmentFactory`, please use `MediaAttachmentFactory` as it has all the functionality of the previous factory while adding additional features such as displaying video previews modifiable number of tiles in a group preview. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) - Started the deprecation process for `ImageAttachmentContent`, please use `MediaAttachmentContent` as it has all the functionality of the previous component while adding additional features such as displaying video previous and modifiable number of tiles in a group preview. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) +- Started the deprecation process for `ImageAttachmentQuotedContent`, please use `MediaAttachmentQuotedContent` as it retains all of the previous functionality while adding the ability to preview video attachments. [#4096](https://github.com/GetStream/stream-chat-android/pull/4096) ### ❌ Removed diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 9ab198355d2..f18bd655d47 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -4,6 +4,7 @@ This document lists deprecated constructs in the SDK, with their expected time | API / Feature | Deprecated (warning) | Deprecated (error) | Removed | Notes | | --- | --- | --- | --- | --- | +| `ImageAttachmentQuotedContent` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | Deprecated in favor of `MediaAttachmentQuotedContent`. The new function has the ability to preview videos as well as images. | | `StreamDimens` constructor containing parameter `attachmentsContentImageGridSpacing` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | This constructor has been deprecated. Use the constructor that does not contain the parameter `attachmentsContentImageGridSpacing`. | | `ImageAttachmentContent` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | `ImageAttachmentContent` has been deprecated in favor of `MediattachmentContent`. The new function is able to preview videos as well as images and has access to a new and improved media gallery. | | `ImageAttachmentFactory` | 2022.09.13
5.9.1 | 2022.09.27
5.9.1 | 2022.10.11 ⌛ | `ImageAttachmentFactory` has been deprecated in favor of `MediaAttachmentFactory`. The new factory is able to preview videos as well as images and has access to a new and improved media gallery. | diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/ImageAttachmentQuotedContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/ImageAttachmentQuotedContent.kt index 52a11cb2bf5..bec722756a5 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/ImageAttachmentQuotedContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/ImageAttachmentQuotedContent.kt @@ -35,6 +35,15 @@ import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter * @param attachment The attachment we wish to show to users. * @param modifier Modifier for styling. */ +@Deprecated( + message = "Deprecated in favor of 'MediaAttachmentQuotedContent'. The new function is able to display previews" + + "for videos as well as images.", + replaceWith = ReplaceWith( + expression = "MediaAttachmentQuotedContent", + "io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentQuotedContent" + ), + level = DeprecationLevel.WARNING +) @Composable public fun ImageAttachmentQuotedContent( attachment: Attachment, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt index 302d49bec1c..8a3af655dd6 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt @@ -27,7 +27,7 @@ import io.getstream.chat.android.uiutils.extension.hasLink import io.getstream.chat.android.uiutils.extension.isFile /** - * An [AttachmentFactory] that validates attachments as files and uses [ImageAttachmentQuotedContent] in case the + * An [AttachmentFactory] that validates attachments as files and uses [MediaAttachmentQuotedContent] in case the * attachment is a media attachment or [FileAttachmentQuotedContent] in case the attachment is a file to build the UI * for the quoted message. */ From ae08b2936b4b512e1987c3844cfa23f3196882b7 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 6 Sep 2022 12:31:34 +0200 Subject: [PATCH 49/69] [3369] Update deprecations --- .../compose/ui/attachments/factory/QuotedAttachmentFactory.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt index 8a3af655dd6..6f21e754823 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactory.kt @@ -19,7 +19,6 @@ package io.getstream.chat.android.compose.ui.attachments.factory import androidx.compose.runtime.Composable import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory import io.getstream.chat.android.compose.ui.attachments.content.FileAttachmentQuotedContent -import io.getstream.chat.android.compose.ui.attachments.content.ImageAttachmentQuotedContent import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentQuotedContent import io.getstream.chat.android.compose.ui.util.isMedia import io.getstream.chat.android.uiutils.constant.AttachmentType From 9147992940caac6b11a35c3cf4fc1596dd33dbd2 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 6 Sep 2022 12:58:35 +0200 Subject: [PATCH 50/69] [3369] Update colours and lessen the frequency of re-evaluating same boolean expressions --- .../api/stream-chat-android-compose.api | 10 ++++++--- .../content/MediaAttachmentContent.kt | 21 ++++++++++++------- .../content/MediaAttachmentQuotedContent.kt | 13 +++++++++--- .../preview/MediaGalleryPreviewActivity.kt | 16 ++++++++------ .../android/compose/ui/theme/StreamColors.kt | 10 +++++++++ 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 081906d3f75..912d6d73dfd 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1596,7 +1596,7 @@ public final class io/getstream/chat/android/compose/ui/theme/ChatThemeKt { public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public static final field Companion Lio/getstream/chat/android/compose/ui/theme/StreamColors$Companion; - public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-0d7_KjU ()J public final fun component10-0d7_KjU ()J public final fun component11-0d7_KjU ()J @@ -1612,6 +1612,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public final fun component20-0d7_KjU ()J public final fun component21-0d7_KjU ()J public final fun component22-0d7_KjU ()J + public final fun component23-0d7_KjU ()J + public final fun component24-0d7_KjU ()J public final fun component3-0d7_KjU ()J public final fun component4-0d7_KjU ()J public final fun component5-0d7_KjU ()J @@ -1619,8 +1621,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public final fun component7-0d7_KjU ()J public final fun component8-0d7_KjU ()J public final fun component9-0d7_KjU ()J - public final fun copy-0VcbP8k (JJJJJJJJJJJJJJJJJJJJJJ)Lio/getstream/chat/android/compose/ui/theme/StreamColors; - public static synthetic fun copy-0VcbP8k$default (Lio/getstream/chat/android/compose/ui/theme/StreamColors;JJJJJJJJJJJJJJJJJJJJJJILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamColors; + public final fun copy-KKJ9vVU (JJJJJJJJJJJJJJJJJJJJJJJJ)Lio/getstream/chat/android/compose/ui/theme/StreamColors; + public static synthetic fun copy-KKJ9vVU$default (Lio/getstream/chat/android/compose/ui/theme/StreamColors;JJJJJJJJJJJJJJJJJJJJJJJJILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamColors; public fun equals (Ljava/lang/Object;)Z public final fun getAppBackground-0d7_KjU ()J public final fun getBarsBackground-0d7_KjU ()J @@ -1644,6 +1646,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public final fun getTextLowEmphasis-0d7_KjU ()J public final fun getThreadSeparatorGradientEnd-0d7_KjU ()J public final fun getThreadSeparatorGradientStart-0d7_KjU ()J + public final fun getVideoBackgroundMediaGalleryPicker-0d7_KjU ()J + public final fun getVideoBackgroundMessageList-0d7_KjU ()J public fun hashCode ()I public fun toString ()Ljava/lang/String; } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 95803bf9887..1e63f1c52cd 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -154,6 +154,7 @@ internal fun ShowSingleMediaAttachment( onLongItemClick: (Message) -> Unit, playButton: @Composable () -> Unit, ) { + val isVideo = attachment.type == AttachmentType.VIDEO // Depending on the CDN, images might not contain their original dimensions val ratio: Float? by remember(key1 = attachment.originalWidth, key2 = attachment.originalHeight) { derivedStateOf { @@ -172,14 +173,14 @@ internal fun ShowSingleMediaAttachment( attachment = attachment, modifier = Modifier .heightIn( - max = if (attachment.type == AttachmentType.VIDEO) { + max = if (isVideo) { ChatTheme.dimens.attachmentsContentVideoMaxHeight } else { ChatTheme.dimens.attachmentsContentImageMaxHeight } ) .width( - if (attachment.type == AttachmentType.VIDEO) { + if (isVideo) { ChatTheme.dimens.attachmentsContentVideoWidth } else { ChatTheme.dimens.attachmentsContentImageWidth @@ -320,6 +321,8 @@ internal fun MediaAttachmentContentItem( playButton: @Composable () -> Unit, ) { val connectionState by ChatClient.instance().clientState.connectionState.collectAsState() + val isImage = attachment.type == AttachmentType.IMAGE + val isVideo = attachment.type == AttachmentType.VIDEO // Used as a workaround for Coil's lack of a retry policy. // See: https://github.com/coil-kt/coil/issues/884#issuecomment-975932886 @@ -328,9 +331,7 @@ internal fun MediaAttachmentContentItem( } val data = - if (attachment.type == AttachmentType.IMAGE || - (attachment.type == AttachmentType.VIDEO && ChatTheme.videoThumbnailsEnabled) - ) { + if (isImage || (isVideo && ChatTheme.videoThumbnailsEnabled)) { attachment.imagePreviewUrl } else { null @@ -378,10 +379,14 @@ internal fun MediaAttachmentContentItem( ), contentAlignment = Alignment.Center ) { + val backgroundColor = + if (isImage) ChatTheme.colors.imageBackgroundMessageList + else ChatTheme.colors.videoBackgroundMessageList + Image( modifier = modifier .fillMaxSize() - .background(ChatTheme.colors.imageBackgroundMessageList), + .background(backgroundColor), painter = painter, contentDescription = null, contentScale = ContentScale.Crop @@ -391,11 +396,11 @@ internal fun MediaAttachmentContentItem( asyncImagePainterState = painter.state, progressIndicatorStrokeWidth = 3.dp, progressIndicatorFillMaxSizePercentage = 0.25f, - isImage = attachment.type == AttachmentType.IMAGE, + isImage = isImage, placeholderIconTintColor = ChatTheme.colors.disabled ) - if (attachment.type == AttachmentType.VIDEO) { + if (isVideo) { playButton() } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt index bd16662db46..25a035a39fc 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt @@ -51,7 +51,13 @@ public fun MediaAttachmentQuotedContent( attachment: Attachment, modifier: Modifier = Modifier, ) { - val imagePainter = rememberStreamImagePainter(attachment.imagePreviewUrl) + val isImage = attachment.type == AttachmentType.IMAGE + val isVideo = attachment.type == AttachmentType.VIDEO + val backgroundColor = + if (isImage) ChatTheme.colors.imageBackgroundMessageList else ChatTheme.colors.videoBackgroundMessageList + + val data = if (isImage || (isVideo && ChatTheme.videoThumbnailsEnabled)) attachment.imagePreviewUrl else null + val imagePainter = rememberStreamImagePainter(data = data) Box( modifier = modifier @@ -68,13 +74,14 @@ public fun MediaAttachmentQuotedContent( Image( modifier = Modifier - .fillMaxSize(1f), + .fillMaxSize(1f) + .background(backgroundColor), painter = imagePainter, contentDescription = null, contentScale = ContentScale.Crop ) - if (attachment.type == AttachmentType.VIDEO) { + if (isVideo) { PlayButton( modifier = Modifier .padding(10.dp) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index a7943a4b8b0..d9bbed6d509 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -1248,6 +1248,9 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { user: User, pagerState: PagerState, ) { + val isImage = attachment.type == AttachmentType.IMAGE + val isVideo = attachment.type == AttachmentType.VIDEO + // Used as a workaround for Coil's lack of a retry policy. // See: https://github.com/coil-kt/coil/issues/884#issuecomment-975932886 var retryHash by remember { @@ -1269,9 +1272,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { contentAlignment = Alignment.Center ) { val data = - if (attachment.type == AttachmentType.IMAGE || - (attachment.type == AttachmentType.VIDEO && ChatTheme.videoThumbnailsEnabled) - ) { + if (isImage || (isVideo && ChatTheme.videoThumbnailsEnabled)) { attachment.imagePreviewUrl } else { null @@ -1290,11 +1291,14 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { retryHash++ } + val backgroundColor = if (isImage) ChatTheme.colors.imageBackgroundMediaGalleryPicker + else ChatTheme.colors.videoBackgroundMediaGalleryPicker + Image( modifier = Modifier .padding(1.dp) .fillMaxSize() - .background(color = ChatTheme.colors.imageBackgroundMediaGalleryPicker), + .background(color = backgroundColor), painter = painter, contentDescription = null, contentScale = ContentScale.Crop @@ -1302,7 +1306,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { MediaPreviewPlaceHolder( asyncImagePainterState = painter.state, - isImage = attachment.type == AttachmentType.IMAGE, + isImage = isImage, progressIndicatorStrokeWidth = 3.dp, progressIndicatorFillMaxSizePercentage = 0.3f ) @@ -1325,7 +1329,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { initials = user.initials ) - if (attachment.type == AttachmentType.VIDEO) { + if (isVideo) { PlayButton( modifier = Modifier .shadow(6.dp, shape = CircleShape) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt index 199e6a999b6..ba5fd9a34e5 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamColors.kt @@ -49,6 +49,10 @@ import io.getstream.chat.android.compose.R * Most visible in placeholders before the images are loaded. * @param imageBackgroundMediaGalleryPicker Used to set the background colour of images inside the media gallery picker * in the media gallery preview screen. Most visible in placeholders before the images are loaded. + * @param imageBackgroundMessageList Used to set the background colour of videos inside the message list. + * Most visible in placeholders before the video previews are loaded. + * @param imageBackgroundMediaGalleryPicker Used to set the background colour of videos inside the media gallery picker + * in the media gallery preview screen. Most visible in placeholders before the videos previews are loaded. */ @Immutable public data class StreamColors( @@ -74,6 +78,8 @@ public data class StreamColors( public val threadSeparatorGradientEnd: Color, public val imageBackgroundMessageList: Color, public val imageBackgroundMediaGalleryPicker: Color, + public val videoBackgroundMessageList: Color, + public val videoBackgroundMediaGalleryPicker: Color, ) { public companion object { @@ -106,6 +112,8 @@ public data class StreamColors( threadSeparatorGradientEnd = colorResource(R.color.stream_compose_app_background), imageBackgroundMessageList = colorResource(R.color.stream_compose_input_background), imageBackgroundMediaGalleryPicker = colorResource(R.color.stream_compose_app_background), + videoBackgroundMessageList = colorResource(R.color.stream_compose_input_background), + videoBackgroundMediaGalleryPicker = colorResource(R.color.stream_compose_app_background), ) /** @@ -137,6 +145,8 @@ public data class StreamColors( threadSeparatorGradientEnd = colorResource(R.color.stream_compose_app_background_dark), imageBackgroundMessageList = colorResource(R.color.stream_compose_input_background_dark), imageBackgroundMediaGalleryPicker = colorResource(R.color.stream_compose_app_background_dark), + videoBackgroundMessageList = colorResource(R.color.stream_compose_input_background_dark), + videoBackgroundMediaGalleryPicker = colorResource(R.color.stream_compose_app_background_dark), ) } } From 321a13bf6ba59fd37e905365b5e1292a8cb453cc Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 6 Sep 2022 13:09:25 +0200 Subject: [PATCH 51/69] [3369] Add giphy support --- .../content/MediaAttachmentQuotedContent.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt index 25a035a39fc..1648df413e3 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt @@ -53,10 +53,18 @@ public fun MediaAttachmentQuotedContent( ) { val isImage = attachment.type == AttachmentType.IMAGE val isVideo = attachment.type == AttachmentType.VIDEO + val isGiphy = attachment.type == AttachmentType.GIPHY + val backgroundColor = if (isImage) ChatTheme.colors.imageBackgroundMessageList else ChatTheme.colors.videoBackgroundMessageList - val data = if (isImage || (isVideo && ChatTheme.videoThumbnailsEnabled)) attachment.imagePreviewUrl else null + val data = + if (isImage || isGiphy || (isVideo && ChatTheme.videoThumbnailsEnabled)) { + attachment.imagePreviewUrl + } else { + null + } + val imagePainter = rememberStreamImagePainter(data = data) Box( From 9266d0f2b8353a221bc4d682575ac280f75c5a2c Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 6 Sep 2022 13:13:23 +0200 Subject: [PATCH 52/69] [3369] Add imgur support --- .../content/MediaAttachmentQuotedContent.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt index 1648df413e3..ba19d62e8f2 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt @@ -51,15 +51,19 @@ public fun MediaAttachmentQuotedContent( attachment: Attachment, modifier: Modifier = Modifier, ) { - val isImage = attachment.type == AttachmentType.IMAGE + val isImageContent = attachment.type == AttachmentType.IMAGE || attachment.type == AttachmentType.IMGUR val isVideo = attachment.type == AttachmentType.VIDEO val isGiphy = attachment.type == AttachmentType.GIPHY val backgroundColor = - if (isImage) ChatTheme.colors.imageBackgroundMessageList else ChatTheme.colors.videoBackgroundMessageList + if (isImageContent || isGiphy) { + ChatTheme.colors.imageBackgroundMessageList + } else { + ChatTheme.colors.videoBackgroundMessageList + } val data = - if (isImage || isGiphy || (isVideo && ChatTheme.videoThumbnailsEnabled)) { + if (isImageContent || isGiphy || (isVideo && ChatTheme.videoThumbnailsEnabled)) { attachment.imagePreviewUrl } else { null From fb11293a1c18f2577d9b78ff0aa68f3539cbbc74 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 6 Sep 2022 13:15:20 +0200 Subject: [PATCH 53/69] [3369] Generate detekt baseline --- stream-chat-android-compose/detekt-baseline.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index 15394c271eb..f6db4b6d4f3 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -2,6 +2,7 @@ + ComplexCondition:MediaAttachmentQuotedContent.kt$isImageContent || isGiphy || (isVideo && ChatTheme.videoThumbnailsEnabled) ComplexCondition:MediaGalleryPreviewActivity.kt$MediaGalleryPreviewActivity$data != null && page == pagerState.currentPage && mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && painter.state is AsyncImagePainter.State.Error ComplexCondition:MessageItem.kt$!messageItem.isMine && ( messageItem.shouldShowFooter || messageItem.groupPosition == Bottom || messageItem.groupPosition == None ) ComplexCondition:MessageOptions.kt$((isOwnMessage && canEditOwnMessage) || canEditAnyMessage) && !selectedMessage.isGiphy() From 5a81c5aeb10691214d4d1e7b4d01bb5fd9d0a60f Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 6 Sep 2022 13:26:24 +0200 Subject: [PATCH 54/69] [3369] Update kdocs to add a guiding hand and add previews --- .../compose/ui/attachments/factory/MediaAttachmentFactory.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt index e057c3df7bb..df8021b028c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory import io.getstream.chat.android.compose.ui.attachments.content.MediaAttachmentContent @@ -37,7 +38,7 @@ import io.getstream.chat.android.uiutils.constant.AttachmentType * An [AttachmentFactory] that is able to handle Image and Video attachments. * * @param maximumNumberOfPreviewedItems The maximum number of thumbnails that can be displayed - * in a group when previewing Media attachments in the message list. + * in a group when previewing Media attachments in the message list. Values between 4 and 8 are optimal. * @param contentPlayButton Displays a play button above video attachments * in the messages list. * @param previewContentPlayButton Displays a play button above video attachments @@ -82,6 +83,7 @@ public fun MediaAttachmentFactory( * overlaid above video attachment previews inside * the messages list. */ +@Preview(name = "DefaultContentPlayButton Preview") @Composable private fun DefaultContentPlayButton() { PlayButton( @@ -99,6 +101,7 @@ private fun DefaultContentPlayButton() { * overlaid above video attachment previews inside * the message input. */ +@Preview(name = "DefaultPreviewContentPlayButton Preview") @Composable private fun DefaultPreviewContentPlayButton() { PlayButton( From f0edbe72232056ed8a5fbabcdb4f9e94bd806615 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 6 Sep 2022 13:39:27 +0200 Subject: [PATCH 55/69] [3369] Improve logical conditions for playback --- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index d9bbed6d509..4baf5c6cec3 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -832,7 +832,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { setOnPreparedListener { // Don't remove the preview unless the user has clicked play previously, // otherwise the preview will be removed whenever the video has finished downloading. - if (!hasPrepared && userHasClickedPlay) { + if (!hasPrepared && userHasClickedPlay && page == pagerState.currentPage) { shouldShowProgressBar = false shouldShowPreview = false mediaController.show() From c607bd55b17d5e4e8e1184dc9d931b40aeef709a Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 13 Sep 2022 18:37:04 +0200 Subject: [PATCH 56/69] [3369] Updated kdocs in `MediaGalleryPreviewActivityAttachmentState.kt` as requested in the PR review --- .../MediaGalleryPreviewActivityAttachmentState.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState.kt index 4408cb7cc64..c6a6648cbcc 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState.kt @@ -18,6 +18,7 @@ package io.getstream.chat.android.compose.state.mediagallerypreview import android.os.Parcelable import io.getstream.chat.android.client.models.Attachment +import io.getstream.chat.android.uiutils.constant.AttachmentType import kotlinx.parcelize.Parcelize /** @@ -26,16 +27,20 @@ import kotlinx.parcelize.Parcelize * for the proper functioning of the Media Gallery Preview screen. * * @param name The name of the attachment. - * @param url The url of the file. + * @param url The URL of the file. * @param thumbUrl The URL for the thumbnail version of the attachment, - * given the attachment has a visual quality, e.g. is a video, an image, - * a link to a website or similar. + * given the attachment has a visual quality, e.g. is a video, an image, + * a link to a website or similar. * @param imageUrl The URL for the raw version of the attachment. - * @param assetUrl The URL for the asset. + * Guaranteed to be non-null for images, optional for other types. + * @param assetUrl The URL for the raw asset, used for various types of + * attachments. * @param originalHeight The original height of the attachment. * Provided if the attachment is of type "image". * @param originalWidth The original width of the attachment. * Provided if the attachment is of type "image". + * @param type The type of the attachment, e.g. "image" or "video". + * @see [AttachmentType] */ @Parcelize internal class MediaGalleryPreviewActivityAttachmentState( From 8b500625aab4c736c225caebf13e08bab112bc76 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 13 Sep 2022 18:46:30 +0200 Subject: [PATCH 57/69] [3369] Added a default value for `AttachmentState.onMediaGalleryPreviewResult` --- .../compose/state/messages/attachments/AttachmentState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt index 72599f217ef..90894dd331b 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/messages/attachments/AttachmentState.kt @@ -45,7 +45,7 @@ constructor( level = DeprecationLevel.WARNING ) val onImagePreviewResult: (ImagePreviewResult?) -> Unit = {}, - val onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit, + val onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, ) { /** From 442436f570b0516313c9d3f9c98eaf120020f193 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 13 Sep 2022 21:00:29 +0200 Subject: [PATCH 58/69] [3369] Added a default value for `AttachmentState.onMediaGalleryPreviewResult` --- .../api/stream-chat-android-compose.api | 28 +++--- .../content/MediaAttachmentContent.kt | 95 +++++++++---------- .../content/MediaAttachmentPreviewContent.kt | 24 +++-- .../factory/MediaAttachmentFactory.kt | 32 ++++--- .../preview/MediaGalleryPreviewActivity.kt | 13 ++- .../android/compose/ui/util/ImageUtils.kt | 8 ++ .../src/main/res/values/strings.xml | 1 + 7 files changed, 114 insertions(+), 87 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 912d6d73dfd..031e1084d9e 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -565,9 +565,16 @@ public final class io/getstream/chat/android/compose/ui/attachments/StreamAttach public final class io/getstream/chat/android/compose/ui/attachments/content/ComposableSingletons$MediaAttachmentContentKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/content/ComposableSingletons$MediaAttachmentContentKt; - public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-1 Lkotlin/jvm/functions/Function3; public fun ()V - public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; +} + +public final class io/getstream/chat/android/compose/ui/attachments/content/ComposableSingletons$MediaAttachmentPreviewContentKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/content/ComposableSingletons$MediaAttachmentPreviewContentKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; } public final class io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContentKt { @@ -610,12 +617,11 @@ public final class io/getstream/chat/android/compose/ui/attachments/content/Link } public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContentKt { - public static final fun MediaAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;ILkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V - public static final fun PlayButton (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public static final fun MediaAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;ILkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContentKt { - public static final fun MediaAttachmentPreviewContent (Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public static final fun MediaAttachmentPreviewContent (Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContentKt { @@ -657,11 +663,11 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Comp public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt; - public static field lambda-1 Lkotlin/jvm/functions/Function2; - public static field lambda-2 Lkotlin/jvm/functions/Function2; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function3; public fun ()V - public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; } public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$QuotedAttachmentFactoryKt { @@ -695,8 +701,8 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Link } public final class io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactoryKt { - public static final fun MediaAttachmentFactory (ILkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; - public static synthetic fun MediaAttachmentFactory$default (ILkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; + public static final fun MediaAttachmentFactory (ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; + public static synthetic fun MediaAttachmentFactory$default (ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; } public final class io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactoryKt { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index 1e63f1c52cd..d8c35a839e1 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -67,6 +67,7 @@ import io.getstream.chat.android.compose.state.messages.attachments.AttachmentSt import io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewContract import io.getstream.chat.android.compose.ui.components.MediaPreviewPlaceHolder import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.util.RETRY_HASH import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter import io.getstream.chat.android.uiutils.constant.AttachmentType import io.getstream.chat.android.uiutils.extension.hasLink @@ -79,8 +80,8 @@ import io.getstream.chat.android.uiutils.extension.hasLink * @param modifier The modifier used for styling. * @param maximumNumberOfPreviewedItems The maximum number of thumbnails that can be displayed * in a group when previewing Media attachments in the message list. - * @param playButton Represents the play button that is overlaid above video attachment - * previews. + * @param itemOverlayContent Represents the content overlaid above individual items. + * By default it is used to display a play button over video previews. */ @OptIn(ExperimentalFoundationApi::class) @Composable @@ -88,7 +89,11 @@ public fun MediaAttachmentContent( attachmentState: AttachmentState, modifier: Modifier = Modifier, maximumNumberOfPreviewedItems: Int = 4, - playButton: @Composable () -> Unit = { PlayButton() }, + itemOverlayContent: @Composable (attachmentType: String?) -> Unit = { attachmentType -> + if (attachmentType == AttachmentType.VIDEO) { + PlayButton() + } + }, ) { val (message, onLongItemClick, _, onMediaGalleryPreviewResult) = attachmentState val gridSpacing = ChatTheme.dimens.attachmentsContentMediaGridSpacing @@ -106,22 +111,22 @@ public fun MediaAttachmentContent( ) { val attachments = message.attachments.filter { - !it.hasLink() && it.type == AttachmentType.IMAGE || it.type == AttachmentType.VIDEO + !it.hasLink() && (it.type == AttachmentType.IMAGE || it.type == AttachmentType.VIDEO) } val attachmentCount = attachments.size if (attachmentCount == 1) { val attachment = attachments.first() - ShowSingleMediaAttachment( + SingleMediaAttachment( attachment = attachment, message = message, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, - playButton = playButton + overlayContent = itemOverlayContent ) } else { - ShowMultipleMediaAttachments( + MultipleMediaAttachments( attachments = attachments, attachmentCount = attachmentCount, gridSpacing = gridSpacing, @@ -129,7 +134,7 @@ public fun MediaAttachmentContent( message = message, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, - playButton = playButton + itemOverlayContent = itemOverlayContent ) } } @@ -143,16 +148,16 @@ public fun MediaAttachmentContent( * @param onMediaGalleryPreviewResult The result of the activity used for propagating * actions such as media attachment selection, deletion, etc. * @param onLongItemClick Lambda that gets called when an item is long clicked. - * @param playButton Represents the play button that is overlaid above video attachment - * previews. + * @param overlayContent Represents the content overlaid above attachment previews. + * Usually used to display a play button over video previews. */ @Composable -internal fun ShowSingleMediaAttachment( +internal fun SingleMediaAttachment( attachment: Attachment, message: Message, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, onLongItemClick: (Message) -> Unit, - playButton: @Composable () -> Unit, + overlayContent: @Composable (attachmentType: String?) -> Unit, ) { val isVideo = attachment.type == AttachmentType.VIDEO // Depending on the CDN, images might not contain their original dimensions @@ -191,7 +196,7 @@ internal fun ShowSingleMediaAttachment( attachmentPosition = 0, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, - playButton = playButton + overlayContent = overlayContent ) } @@ -207,12 +212,12 @@ internal fun ShowSingleMediaAttachment( * @param onMediaGalleryPreviewResult The result of the activity used for propagating * actions such as media attachment selection, deletion, etc. * @param onLongItemClick Lambda that gets called when an item is long clicked. - * @param playButton Represents the play button that is overlaid above video attachment - * previews. + * @param itemOverlayContent Represents the content overlaid above individual items. + * Usually used to display a play button over video previews. */ @Suppress("LongParameterList", "LongMethod") @Composable -internal fun RowScope.ShowMultipleMediaAttachments( +internal fun RowScope.MultipleMediaAttachments( attachments: List, attachmentCount: Int, gridSpacing: Dp, @@ -220,7 +225,7 @@ internal fun RowScope.ShowMultipleMediaAttachments( message: Message, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, onLongItemClick: (Message) -> Unit, - playButton: @Composable () -> Unit, + itemOverlayContent: @Composable (attachmentType: String?) -> Unit, ) { Column( @@ -239,7 +244,7 @@ internal fun RowScope.ShowMultipleMediaAttachments( attachmentPosition = attachmentIndex, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, - playButton = playButton + overlayContent = itemOverlayContent ) } } @@ -266,7 +271,7 @@ internal fun RowScope.ShowMultipleMediaAttachments( attachmentPosition = attachmentIndex, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, - playButton = playButton + overlayContent = itemOverlayContent ) if (!isUploading) { @@ -285,7 +290,7 @@ internal fun RowScope.ShowMultipleMediaAttachments( attachmentPosition = attachmentIndex, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onLongItemClick = onLongItemClick, - playButton = playButton + overlayContent = itemOverlayContent ) } } @@ -305,8 +310,8 @@ internal fun RowScope.ShowMultipleMediaAttachments( * actions such as media attachment selection, deletion, etc. * @param onLongItemClick Lambda that gets called when the item is long clicked. * @param modifier Modifier used for styling. - * @param playButton Represents the play button that is overlaid above video attachment - * previews. + * @param overlayContent Represents the content overlaid above attachment previews. + * Usually used to display a play button over video previews. */ @Suppress("LongParameterList") @OptIn(ExperimentalFoundationApi::class) @@ -318,7 +323,7 @@ internal fun MediaAttachmentContentItem( onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit, onLongItemClick: (Message) -> Unit, modifier: Modifier = Modifier, - playButton: @Composable () -> Unit, + overlayContent: @Composable (attachmentType: String?) -> Unit, ) { val connectionState by ChatClient.instance().clientState.connectionState.collectAsState() val isImage = attachment.type == AttachmentType.IMAGE @@ -340,7 +345,7 @@ internal fun MediaAttachmentContentItem( val painter = rememberStreamImagePainter( model = ImageRequest.Builder(LocalContext.current) .data(data) - .setParameter(key = "retry_hash", value = retryHash) + .setParameter(key = RETRY_HASH, value = retryHash) .build() ) @@ -400,8 +405,8 @@ internal fun MediaAttachmentContentItem( placeholderIconTintColor = ChatTheme.colors.disabled ) - if (isVideo) { - playButton() + if (painter.state !is AsyncImagePainter.State.Loading) { + overlayContent(attachment.type) } } } @@ -411,28 +416,28 @@ internal fun MediaAttachmentContentItem( * video attachments. * * @param modifier The modifier used for styling. + * @param contentDescription Used to describe the content represented by this composable. */ @Composable -public fun PlayButton( +internal fun PlayButton( modifier: Modifier = Modifier, + contentDescription: String? = null, ) { - Box( + Column( modifier = modifier, - contentAlignment = Alignment.Center + verticalArrangement = Arrangement.Center ) { - Column { - Image( - modifier = Modifier - .fillMaxSize(0.85f) - .alignBy { measured -> - // emulated offset as seen in the design specs, - // otherwise the button is visibly off to the start of the screen - -(measured.measuredWidth * 1 / 8) - }, - painter = painterResource(id = R.drawable.stream_compose_ic_play), - contentDescription = null, - ) - } + Image( + modifier = Modifier + .fillMaxSize(0.85f) + .alignBy { measured -> + // emulated offset as seen in the design specs, + // otherwise the button is visibly off to the start of the screen + -(measured.measuredWidth * 1 / 6) + }, + painter = painterResource(id = R.drawable.stream_compose_ic_play), + contentDescription = contentDescription, + ) } } @@ -477,9 +482,3 @@ internal fun MediaAttachmentViewMoreOverlay( * Composable when calling [Modifier.aspectRatio]. */ private const val EqualDimensionsRatio = 1f - -/** - * Produces a height value that is twice the width of the - * Composable when calling [Modifier.aspectRatio]. - */ -private const val TwiceAsTallAsIsWideRatio = 0.5f diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt index c62dba34f09..2fdfd8fa0bf 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt @@ -33,8 +33,10 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import com.getstream.sdk.chat.utils.extensions.imagePreviewUrl import io.getstream.chat.android.client.models.Attachment +import io.getstream.chat.android.compose.ui.attachments.factory.DefaultPreviewItemOverlayContent import io.getstream.chat.android.compose.ui.components.CancelIcon import io.getstream.chat.android.compose.ui.components.composer.MessageInput +import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter import io.getstream.chat.android.uiutils.constant.AttachmentType @@ -44,16 +46,22 @@ import io.getstream.chat.android.uiutils.constant.AttachmentType * @param attachments Selected attachments. * @param onAttachmentRemoved Handler when the user removes an attachment from the list. * @param modifier Modifier for styling. + * @param previewItemOverlayContent Represents the content overlaid above individual preview items. + * By default it is used to display a play button over video previews. */ @Composable public fun MediaAttachmentPreviewContent( attachments: List, onAttachmentRemoved: (Attachment) -> Unit, - playButton: @Composable () -> Unit, modifier: Modifier = Modifier, + previewItemOverlayContent: @Composable (attachmentType: String?) -> Unit = { attachmentType -> + if (attachmentType == AttachmentType.VIDEO) { + DefaultPreviewItemOverlayContent() + } + }, ) { LazyRow( - modifier = modifier.clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)), + modifier = modifier.clip(ChatTheme.shapes.attachment), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start) ) { @@ -61,7 +69,7 @@ public fun MediaAttachmentPreviewContent( MediaAttachmentPreviewItem( mediaAttachment = image, onAttachmentRemoved = onAttachmentRemoved, - playButton = playButton + overlayContent = previewItemOverlayContent ) } } @@ -72,14 +80,14 @@ public fun MediaAttachmentPreviewContent( * * @param mediaAttachment The selected attachment. * @param onAttachmentRemoved Handler when the user removes an attachment from the list. - * @param playButton Represents the play button that is overlaid above video attachment - * previews. + * @param overlayContent Represents the content overlaid above the item. + * Usually used to display an icon above video previews. */ @Composable private fun MediaAttachmentPreviewItem( mediaAttachment: Attachment, onAttachmentRemoved: (Attachment) -> Unit, - playButton: @Composable () -> Unit, + overlayContent: @Composable (attachmentType: String?) -> Unit, ) { val painter = rememberStreamImagePainter(data = mediaAttachment.upload ?: mediaAttachment.imagePreviewUrl) @@ -96,9 +104,7 @@ private fun MediaAttachmentPreviewItem( contentScale = ContentScale.Crop ) - if (mediaAttachment.type == AttachmentType.VIDEO) { - playButton() - } + overlayContent(mediaAttachment.type) CancelIcon( modifier = Modifier diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt index df8021b028c..92ec7366e88 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt @@ -39,19 +39,23 @@ import io.getstream.chat.android.uiutils.constant.AttachmentType * * @param maximumNumberOfPreviewedItems The maximum number of thumbnails that can be displayed * in a group when previewing Media attachments in the message list. Values between 4 and 8 are optimal. - * @param contentPlayButton Displays a play button above video attachments - * in the messages list. - * @param previewContentPlayButton Displays a play button above video attachments - * in the message input. + * @param itemOverlayContent Represents the content overlaid above individual items. + * By default it is used to display a play button over video previews. + * @param previewItemOverlayContent Represents the content overlaid above individual preview items. + * By default it is used to display a play button over video previews. */ @Suppress("FunctionName") public fun MediaAttachmentFactory( maximumNumberOfPreviewedItems: Int = 4, - contentPlayButton: @Composable () -> Unit = { - DefaultContentPlayButton() + itemOverlayContent: @Composable (attachmentType: String?) -> Unit = { attachmentType -> + if (attachmentType == AttachmentType.VIDEO) { + DefaultItemOverlayContent() + } }, - previewContentPlayButton: @Composable () -> Unit = { - DefaultPreviewContentPlayButton() + previewItemOverlayContent: @Composable (attachmentType: String?) -> Unit = { attachmentType -> + if (attachmentType == AttachmentType.VIDEO) { + DefaultPreviewItemOverlayContent() + } }, ): AttachmentFactory = AttachmentFactory( @@ -65,7 +69,7 @@ public fun MediaAttachmentFactory( attachments = attachments, onAttachmentRemoved = onAttachmentRemoved, modifier = modifier, - playButton = previewContentPlayButton + previewItemOverlayContent = previewItemOverlayContent ) }, content = @Composable { modifier, state -> @@ -73,7 +77,7 @@ public fun MediaAttachmentFactory( modifier = modifier, attachmentState = state, maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, - playButton = { contentPlayButton() } + itemOverlayContent = itemOverlayContent ) } ) @@ -83,9 +87,9 @@ public fun MediaAttachmentFactory( * overlaid above video attachment previews inside * the messages list. */ -@Preview(name = "DefaultContentPlayButton Preview") +@Preview(name = "DefaultItemOverlayContent Preview") @Composable -private fun DefaultContentPlayButton() { +private fun DefaultItemOverlayContent() { PlayButton( modifier = Modifier .padding(2.dp) @@ -101,9 +105,9 @@ private fun DefaultContentPlayButton() { * overlaid above video attachment previews inside * the message input. */ -@Preview(name = "DefaultPreviewContentPlayButton Preview") +@Preview(name = "DefaultPreviewItemOverlayContent Preview") @Composable -private fun DefaultPreviewContentPlayButton() { +internal fun DefaultPreviewItemOverlayContent() { PlayButton( modifier = Modifier .shadow(6.dp, shape = CircleShape) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 4baf5c6cec3..ffcc6bc63b0 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -151,6 +151,7 @@ import io.getstream.chat.android.compose.ui.components.NetworkLoadingIndicator import io.getstream.chat.android.compose.ui.components.Timestamp import io.getstream.chat.android.compose.ui.components.avatar.Avatar import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.util.RETRY_HASH import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter import io.getstream.chat.android.compose.viewmodel.mediapreview.MediaGalleryPreviewViewModel import io.getstream.chat.android.compose.viewmodel.mediapreview.MediaGalleryPreviewViewModelFactory @@ -641,7 +642,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { model = ImageRequest.Builder(LocalContext.current) .data(data) .crossfade(true) - .setParameter(key = "retry_hash", value = retryHash) + .setParameter(key = RETRY_HASH, value = retryHash) .build() ) @@ -904,7 +905,8 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { .size( width = 42.dp, height = 42.dp - ) + ), + contentDescription = getString(R.string.stream_compose_cd_play_button) ) } } @@ -1281,7 +1283,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { val painter = rememberStreamImagePainter( model = ImageRequest.Builder(LocalContext.current) .data(data) - .setHeader("retry_hash", retryHash.toString()) + .setParameter(RETRY_HASH, retryHash.toString()) .build() ) @@ -1329,12 +1331,13 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { initials = user.initials ) - if (isVideo) { + if (isVideo && painter.state !is AsyncImagePainter.State.Loading) { PlayButton( modifier = Modifier .shadow(6.dp, shape = CircleShape) .background(color = Color.White, shape = CircleShape) - .fillMaxSize(0.2f) + .fillMaxSize(0.2f), + contentDescription = getString(R.string.stream_compose_cd_play_button) ) } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt index 9abca63490d..f011f80765f 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt @@ -157,3 +157,11 @@ public fun rememberStreamImagePainter( filterQuality = filterQuality, ) } + +/** + * Used to change a parameter set on Coil requests in order + * to force Coil into retrying a request. + * + * See: https://github.com/coil-kt/coil/issues/884#issuecomment-975932886 + */ +internal const val RETRY_HASH: String = "retry_hash" diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index 8c9a4bbfae3..f4f9b2aa8dc 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -173,4 +173,5 @@ Message item Message input Send button + Play button From f03d68118571ef1f368763fb8bc458da30539ecb Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 13 Sep 2022 21:01:08 +0200 Subject: [PATCH 59/69] [3369] Generate an api dump --- stream-chat-android-compose/api/stream-chat-android-compose.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 031e1084d9e..af448538e38 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -621,7 +621,7 @@ public final class io/getstream/chat/android/compose/ui/attachments/content/Medi } public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContentKt { - public static final fun MediaAttachmentPreviewContent (Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public static final fun MediaAttachmentPreviewContent (Ljava/util/List;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContentKt { From afd03465f8c8e299dc02d3186cad57abb48bee18 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 13 Sep 2022 21:03:21 +0200 Subject: [PATCH 60/69] [3369] Appease Detekt --- .../ui/attachments/content/MediaAttachmentContent.kt | 7 ++++--- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 6 +++--- .../getstream/chat/android/compose/ui/util/ImageUtils.kt | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt index d8c35a839e1..3ef64133153 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -67,7 +67,7 @@ import io.getstream.chat.android.compose.state.messages.attachments.AttachmentSt import io.getstream.chat.android.compose.ui.attachments.preview.MediaGalleryPreviewContract import io.getstream.chat.android.compose.ui.components.MediaPreviewPlaceHolder import io.getstream.chat.android.compose.ui.theme.ChatTheme -import io.getstream.chat.android.compose.ui.util.RETRY_HASH +import io.getstream.chat.android.compose.ui.util.RetryHash import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter import io.getstream.chat.android.uiutils.constant.AttachmentType import io.getstream.chat.android.uiutils.extension.hasLink @@ -313,7 +313,7 @@ internal fun RowScope.MultipleMediaAttachments( * @param overlayContent Represents the content overlaid above attachment previews. * Usually used to display a play button over video previews. */ -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LongMethod") @OptIn(ExperimentalFoundationApi::class) @Composable internal fun MediaAttachmentContentItem( @@ -345,7 +345,7 @@ internal fun MediaAttachmentContentItem( val painter = rememberStreamImagePainter( model = ImageRequest.Builder(LocalContext.current) .data(data) - .setParameter(key = RETRY_HASH, value = retryHash) + .setParameter(key = RetryHash, value = retryHash) .build() ) @@ -418,6 +418,7 @@ internal fun MediaAttachmentContentItem( * @param modifier The modifier used for styling. * @param contentDescription Used to describe the content represented by this composable. */ +@Suppress("MagicNumber") @Composable internal fun PlayButton( modifier: Modifier = Modifier, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index ffcc6bc63b0..0ac34be8566 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -151,7 +151,7 @@ import io.getstream.chat.android.compose.ui.components.NetworkLoadingIndicator import io.getstream.chat.android.compose.ui.components.Timestamp import io.getstream.chat.android.compose.ui.components.avatar.Avatar import io.getstream.chat.android.compose.ui.theme.ChatTheme -import io.getstream.chat.android.compose.ui.util.RETRY_HASH +import io.getstream.chat.android.compose.ui.util.RetryHash import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter import io.getstream.chat.android.compose.viewmodel.mediapreview.MediaGalleryPreviewViewModel import io.getstream.chat.android.compose.viewmodel.mediapreview.MediaGalleryPreviewViewModelFactory @@ -642,7 +642,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { model = ImageRequest.Builder(LocalContext.current) .data(data) .crossfade(true) - .setParameter(key = RETRY_HASH, value = retryHash) + .setParameter(key = RetryHash, value = retryHash) .build() ) @@ -1283,7 +1283,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { val painter = rememberStreamImagePainter( model = ImageRequest.Builder(LocalContext.current) .data(data) - .setParameter(RETRY_HASH, retryHash.toString()) + .setParameter(RetryHash, retryHash.toString()) .build() ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt index f011f80765f..7a7ba4617d0 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ImageUtils.kt @@ -164,4 +164,4 @@ public fun rememberStreamImagePainter( * * See: https://github.com/coil-kt/coil/issues/884#issuecomment-975932886 */ -internal const val RETRY_HASH: String = "retry_hash" +internal const val RetryHash: String = "retry_hash" From 5efde88d42277a52120012a0e1248053b10f39ab Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Tue, 13 Sep 2022 21:07:43 +0200 Subject: [PATCH 61/69] [3369] Remove useless TODO for an already completed task --- .../getstream/chat/android/compose/sample/ui/MessagesActivity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt index 6d65edd2d6e..86ffb8cfe10 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/ui/MessagesActivity.kt @@ -131,7 +131,6 @@ class MessagesActivity : BaseConnectedActivity() { composerViewModel.setMessageMode(MessageMode.MessageThread(message)) listViewModel.openMessageThread(message) }, - // TODO edit docs onMediaGalleryPreviewResult = { result -> when (result?.resultType) { MediaGalleryPreviewResultType.QUOTE -> { From 21279543b19c89d2cd136818d8715333d1e223a7 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Fri, 16 Sep 2022 14:15:54 +0200 Subject: [PATCH 62/69] [3369] Introduce UI feedback for sharing and cancellable sharing --- .../preview/MediaGalleryPreviewActivity.kt | 72 +++++++++++++++---- .../src/main/res/values/strings.xml | 3 +- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 0ac34be8566..25c13779086 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -80,6 +80,7 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Scaffold @@ -156,6 +157,7 @@ import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter import io.getstream.chat.android.compose.viewmodel.mediapreview.MediaGalleryPreviewViewModel import io.getstream.chat.android.compose.viewmodel.mediapreview.MediaGalleryPreviewViewModelFactory import io.getstream.chat.android.uiutils.constant.AttachmentType +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.util.Date import kotlin.math.abs @@ -179,6 +181,16 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { ) } + /** + * Holds a job used to share an image or a file. + */ + private var fileSharingJob: Job? = null + + /** + * Indicated that we are preparing a file for sharing. + */ + private var isSharingInProgress: Boolean by mutableStateOf(false) + /** * The ViewModel that exposes screen data. */ @@ -986,12 +998,26 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { ) { IconButton( modifier = Modifier.align(Alignment.CenterStart), - onClick = { onShareMediaClick(attachments[pagerState.currentPage]) }, + onClick = { + if (isSharingInProgress) { + fileSharingJob?.cancel() + isSharingInProgress = false + } else { + onShareMediaClick(attachments[pagerState.currentPage]) + } + }, enabled = mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && !isCurrentAttachmentVideo ) { + + val shareIcon = if (!isSharingInProgress) { + R.drawable.stream_compose_ic_share + } else { + R.drawable.stream_compose_ic_clear + } + Icon( - painter = painterResource(id = R.drawable.stream_compose_ic_share), + painter = painterResource(id = shareIcon), contentDescription = stringResource(id = R.string.stream_compose_image_preview_share), tint = if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && !isCurrentAttachmentVideo @@ -1003,16 +1029,36 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { ) } - Text( + Row( modifier = Modifier.align(Alignment.Center), - text = stringResource( - id = R.string.stream_compose_image_order, - pagerState.currentPage + 1, - attachmentCount - ), - style = ChatTheme.typography.title3Bold, - color = ChatTheme.colors.textHighEmphasis - ) + verticalAlignment = Alignment.CenterVertically + ) { + if (isSharingInProgress) { + CircularProgressIndicator( + modifier = Modifier + .padding(horizontal = 12.dp) + .size(24.dp), + strokeWidth = 2.dp, + color = ChatTheme.colors.primaryAccent, + ) + } + + val text = if (!isSharingInProgress) { + stringResource( + id = R.string.stream_compose_image_order, + pagerState.currentPage + 1, + attachmentCount + ) + } else { + stringResource(id = R.string.stream_compose_media_gallery_preview_preparing) + } + + Text( + text = text, + style = ChatTheme.typography.title3Bold, + color = ChatTheme.colors.textHighEmphasis + ) + } IconButton( modifier = Modifier.align(Alignment.CenterEnd), @@ -1109,7 +1155,8 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { */ private fun onShareMediaClick(attachment: Attachment) { // TODO share videos as well - lifecycleScope.launch { + fileSharingJob = lifecycleScope.launch { + isSharingInProgress = true val uri = StreamImageLoader.instance().loadAsBitmap( context = applicationContext, url = attachment.imagePreviewUrl!! @@ -1118,6 +1165,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { } if (uri != null) { + isSharingInProgress = false shareImage(uri) } } diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index f4f9b2aa8dc..ad26b8f8dd5 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -167,11 +167,12 @@ Delete Share Photos + Preparing... Channel item Message item Message input Send button - Play button + Play button From 9bac20c379e522ebb6d1b2751d5638b7c7b80175 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Fri, 16 Sep 2022 14:17:35 +0200 Subject: [PATCH 63/69] [3369] Suppress Detekt --- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 25c13779086..100a6824391 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -979,6 +979,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { * @param attachments The attachments to use for the UI state and options. * @param pagerState The state of the pager, used for current page information. */ + @Suppress("LongMethod") @Composable private fun MediaGalleryPreviewBottomBar(attachments: List, pagerState: PagerState) { val attachmentCount = attachments.size From d494d69ea11470ea80749fce9aef64568bc263db Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Fri, 16 Sep 2022 14:34:45 +0200 Subject: [PATCH 64/69] [3369] Pulled dev and resolved merge conflicts --- .../api/stream-chat-android-compose.api | 185 +++++++++++++++++- .../android/compose/ui/theme/StreamDimens.kt | 10 +- 2 files changed, 180 insertions(+), 15 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 94fd8d9345a..09f69dd878c 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -190,6 +190,47 @@ public final class io/getstream/chat/android/compose/state/imagepreview/ImagePre public static fun values ()[Lio/getstream/chat/android/compose/state/imagepreview/ImagePreviewResultType; } +public final class io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityState$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityState; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityState; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult : android/os/Parcelable { + public static final field $stable I + public static final field CREATOR Landroid/os/Parcelable$Creator; + public fun (Ljava/lang/String;Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType;)V + public fun describeContents ()I + public final fun getMessageId ()Ljava/lang/String; + public final fun getResultType ()Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType; + public fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType : java/lang/Enum { + public static final field QUOTE Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType; + public static final field SHOW_IN_CHAT Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType; + public static fun valueOf (Ljava/lang/String;)Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType; + public static fun values ()[Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResultType; +} + public final class io/getstream/chat/android/compose/state/messageoptions/MessageOptionItemState { public static final field $stable I public synthetic fun (IJLandroidx/compose/ui/graphics/painter/Painter;JLio/getstream/chat/android/common/state/MessageAction;Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -300,15 +341,19 @@ public final class io/getstream/chat/android/compose/state/messages/attachments/ public static final field $stable I public fun (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V public synthetic fun (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/client/models/Message; public final fun component2 ()Lkotlin/jvm/functions/Function1; public final fun component3 ()Lkotlin/jvm/functions/Function1; - public final fun copy (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState; + public final fun component4 ()Lkotlin/jvm/functions/Function1; + public final fun copy (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState; public fun equals (Ljava/lang/Object;)Z public final fun getMessage ()Lio/getstream/chat/android/client/models/Message; public final fun getOnImagePreviewResult ()Lkotlin/jvm/functions/Function1; public final fun getOnLongItemClick ()Lkotlin/jvm/functions/Function1; + public final fun getOnMediaGalleryPreviewResult ()Lkotlin/jvm/functions/Function1; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -518,6 +563,20 @@ public final class io/getstream/chat/android/compose/ui/attachments/StreamAttach public final fun defaultQuotedFactories ()Ljava/util/List; } +public final class io/getstream/chat/android/compose/ui/attachments/content/ComposableSingletons$MediaAttachmentContentKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/content/ComposableSingletons$MediaAttachmentContentKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; +} + +public final class io/getstream/chat/android/compose/ui/attachments/content/ComposableSingletons$MediaAttachmentPreviewContentKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/content/ComposableSingletons$MediaAttachmentPreviewContentKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; +} + public final class io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContentKt { public static final fun FileAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V public static final fun FileAttachmentImage (Lio/getstream/chat/android/client/models/Attachment;Landroidx/compose/runtime/Composer;I)V @@ -557,6 +616,18 @@ public final class io/getstream/chat/android/compose/ui/attachments/content/Link public static final fun LinkAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;ILandroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } +public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContentKt { + public static final fun MediaAttachmentContent (Lio/getstream/chat/android/compose/state/messages/attachments/AttachmentState;Landroidx/compose/ui/Modifier;ILkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +} + +public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContentKt { + public static final fun MediaAttachmentPreviewContent (Ljava/util/List;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +} + +public final class io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContentKt { + public static final fun MediaAttachmentQuotedContent (Lio/getstream/chat/android/client/models/Attachment;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + public final class io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContentKt { public static final fun MessageAttachmentsContent (Lio/getstream/chat/android/client/models/Message;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } @@ -583,6 +654,15 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Comp public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function4; } +public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$MediaAttachmentFactoryKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; +} + public final class io/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$QuotedAttachmentFactoryKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/factory/ComposableSingletons$QuotedAttachmentFactoryKt; public static field lambda-1 Lkotlin/jvm/functions/Function4; @@ -614,6 +694,11 @@ public final class io/getstream/chat/android/compose/ui/attachments/factory/Link public static final fun LinkAttachmentFactory (I)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; } +public final class io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactoryKt { + public static final fun MediaAttachmentFactory (ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; + public static synthetic fun MediaAttachmentFactory$default (ILkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; +} + public final class io/getstream/chat/android/compose/ui/attachments/factory/QuotedAttachmentFactoryKt { public static final fun QuotedAttachmentFactory ()Lio/getstream/chat/android/compose/ui/attachments/AttachmentFactory; } @@ -633,6 +718,15 @@ public final class io/getstream/chat/android/compose/ui/attachments/preview/Comp public final fun getLambda-3$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; } +public final class io/getstream/chat/android/compose/ui/attachments/preview/ComposableSingletons$MediaGalleryPreviewActivityKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/preview/ComposableSingletons$MediaGalleryPreviewActivityKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; +} + public final class io/getstream/chat/android/compose/ui/attachments/preview/ComposableSingletons$MediaPreviewActivityKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/attachments/preview/ComposableSingletons$MediaPreviewActivityKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; @@ -668,6 +762,35 @@ public final class io/getstream/chat/android/compose/ui/attachments/preview/Imag public final fun getMessageId ()Ljava/lang/String; } +public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity : androidx/appcompat/app/AppCompatActivity { + public static final field $stable I + public static final field Companion Lio/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity$Companion; + public static final field KeyMediaGalleryPreviewResult Ljava/lang/String; + public fun ()V +} + +public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity$Companion { + public final fun getIntent (Landroid/content/Context;Lio/getstream/chat/android/client/models/Message;IZ)Landroid/content/Intent; +} + +public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract : androidx/activity/result/contract/ActivityResultContract { + public static final field $stable I + public fun ()V + public fun createIntent (Landroid/content/Context;Lio/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract$Input;)Landroid/content/Intent; + public synthetic fun createIntent (Landroid/content/Context;Ljava/lang/Object;)Landroid/content/Intent; + public fun parseResult (ILandroid/content/Intent;)Lio/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewResult; + public synthetic fun parseResult (ILandroid/content/Intent;)Ljava/lang/Object; +} + +public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract$Input { + public static final field $stable I + public fun (Lio/getstream/chat/android/client/models/Message;IZ)V + public synthetic fun (Lio/getstream/chat/android/client/models/Message;IZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getInitialPosition ()I + public final fun getMessage ()Lio/getstream/chat/android/client/models/Message; + public final fun getVideoThumbnailsEnabled ()Z +} + public final class io/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity : androidx/appcompat/app/AppCompatActivity { public static final field $stable I public static final field Companion Lio/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity$Companion; @@ -1464,15 +1587,16 @@ public final class io/getstream/chat/android/compose/ui/theme/ChatTheme { public final fun getReactionIconFactory (Landroidx/compose/runtime/Composer;I)Lio/getstream/chat/android/compose/ui/util/ReactionIconFactory; public final fun getShapes (Landroidx/compose/runtime/Composer;I)Lio/getstream/chat/android/compose/ui/theme/StreamShapes; public final fun getTypography (Landroidx/compose/runtime/Composer;I)Lio/getstream/chat/android/compose/ui/theme/StreamTypography; + public final fun getVideoThumbnailsEnabled (Landroidx/compose/runtime/Composer;I)Z } public final class io/getstream/chat/android/compose/ui/theme/ChatThemeKt { - public static final fun ChatTheme (ZLio/getstream/chat/android/compose/ui/theme/StreamColors;Lio/getstream/chat/android/compose/ui/theme/StreamDimens;Lio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamShapes;Landroidx/compose/material/ripple/RippleTheme;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/compose/ui/util/ReactionIconFactory;Lcom/getstream/sdk/chat/utils/DateFormatter;Lio/getstream/chat/android/compose/ui/util/ChannelNameFormatter;Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter;Lio/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory;Lio/getstream/chat/android/compose/ui/util/MessageAlignmentProvider;Lio/getstream/chat/android/common/MessageOptionsUserReactionAlignment;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V + public static final fun ChatTheme (ZLio/getstream/chat/android/compose/ui/theme/StreamColors;Lio/getstream/chat/android/compose/ui/theme/StreamDimens;Lio/getstream/chat/android/compose/ui/theme/StreamTypography;Lio/getstream/chat/android/compose/ui/theme/StreamShapes;Landroidx/compose/material/ripple/RippleTheme;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lio/getstream/chat/android/compose/ui/util/ReactionIconFactory;Lcom/getstream/sdk/chat/utils/DateFormatter;Lio/getstream/chat/android/compose/ui/util/ChannelNameFormatter;Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter;Lio/getstream/chat/android/compose/ui/util/StreamCoilImageLoaderFactory;Lio/getstream/chat/android/compose/ui/util/MessageAlignmentProvider;Lio/getstream/chat/android/common/MessageOptionsUserReactionAlignment;Ljava/util/List;Ljava/util/List;ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public static final field Companion Lio/getstream/chat/android/compose/ui/theme/StreamColors$Companion; - public synthetic fun (JJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-0d7_KjU ()J public final fun component10-0d7_KjU ()J public final fun component11-0d7_KjU ()J @@ -1486,6 +1610,10 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public final fun component19-0d7_KjU ()J public final fun component2-0d7_KjU ()J public final fun component20-0d7_KjU ()J + public final fun component21-0d7_KjU ()J + public final fun component22-0d7_KjU ()J + public final fun component23-0d7_KjU ()J + public final fun component24-0d7_KjU ()J public final fun component3-0d7_KjU ()J public final fun component4-0d7_KjU ()J public final fun component5-0d7_KjU ()J @@ -1493,8 +1621,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public final fun component7-0d7_KjU ()J public final fun component8-0d7_KjU ()J public final fun component9-0d7_KjU ()J - public final fun copy-Cmkg8xs (JJJJJJJJJJJJJJJJJJJJ)Lio/getstream/chat/android/compose/ui/theme/StreamColors; - public static synthetic fun copy-Cmkg8xs$default (Lio/getstream/chat/android/compose/ui/theme/StreamColors;JJJJJJJJJJJJJJJJJJJJILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamColors; + public final fun copy-KKJ9vVU (JJJJJJJJJJJJJJJJJJJJJJJJ)Lio/getstream/chat/android/compose/ui/theme/StreamColors; + public static synthetic fun copy-KKJ9vVU$default (Lio/getstream/chat/android/compose/ui/theme/StreamColors;JJJJJJJJJJJJJJJJJJJJJJJJILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamColors; public fun equals (Ljava/lang/Object;)Z public final fun getAppBackground-0d7_KjU ()J public final fun getBarsBackground-0d7_KjU ()J @@ -1504,6 +1632,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public final fun getErrorAccent-0d7_KjU ()J public final fun getGiphyMessageBackground-0d7_KjU ()J public final fun getHighlight-0d7_KjU ()J + public final fun getImageBackgroundMediaGalleryPicker-0d7_KjU ()J + public final fun getImageBackgroundMessageList-0d7_KjU ()J public final fun getInfoAccent-0d7_KjU ()J public final fun getInputBackground-0d7_KjU ()J public final fun getLinkBackground-0d7_KjU ()J @@ -1516,6 +1646,8 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors { public final fun getTextLowEmphasis-0d7_KjU ()J public final fun getThreadSeparatorGradientEnd-0d7_KjU ()J public final fun getThreadSeparatorGradientStart-0d7_KjU ()J + public final fun getVideoBackgroundMediaGalleryPicker-0d7_KjU ()J + public final fun getVideoBackgroundMessageList-0d7_KjU ()J public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1527,8 +1659,10 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamColors$Compa public final class io/getstream/chat/android/compose/ui/theme/StreamDimens { public static final field Companion Lio/getstream/chat/android/compose/ui/theme/StreamDimens$Companion; - public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFLkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1-D9Ej5fM ()F public final fun component10-D9Ej5fM ()F public final fun component11-D9Ej5fM ()F @@ -1569,13 +1703,18 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamDimens { public final fun component43-D9Ej5fM ()F public final fun component44-D9Ej5fM ()F public final fun component45-D9Ej5fM ()F + public final fun component46-D9Ej5fM ()F + public final fun component47-D9Ej5fM ()F + public final fun component48-D9Ej5fM ()F + public final fun component49-D9Ej5fM ()F public final fun component5-D9Ej5fM ()F + public final fun component50-D9Ej5fM ()F public final fun component6-D9Ej5fM ()F public final fun component7-D9Ej5fM ()F public final fun component8-D9Ej5fM ()F public final fun component9-D9Ej5fM ()F - public final fun copy-FIEVJyw (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; - public static synthetic fun copy-FIEVJyw$default (Lio/getstream/chat/android/compose/ui/theme/StreamDimens;FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; + public final fun copy-1ChE538 (FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; + public static synthetic fun copy-1ChE538$default (Lio/getstream/chat/android/compose/ui/theme/StreamDimens;FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFIILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/theme/StreamDimens; public fun equals (Ljava/lang/Object;)Z public final fun getAttachmentsContentFileUploadWidth-D9Ej5fM ()F public final fun getAttachmentsContentFileWidth-D9Ej5fM ()F @@ -1583,10 +1722,15 @@ public final class io/getstream/chat/android/compose/ui/theme/StreamDimens { public final fun getAttachmentsContentGiphyMaxHeight-D9Ej5fM ()F public final fun getAttachmentsContentGiphyMaxWidth-D9Ej5fM ()F public final fun getAttachmentsContentGiphyWidth-D9Ej5fM ()F + public final fun getAttachmentsContentGroupPreviewHeight-D9Ej5fM ()F + public final fun getAttachmentsContentGroupPreviewWidth-D9Ej5fM ()F public final fun getAttachmentsContentImageGridSpacing-D9Ej5fM ()F public final fun getAttachmentsContentImageMaxHeight-D9Ej5fM ()F public final fun getAttachmentsContentImageWidth-D9Ej5fM ()F public final fun getAttachmentsContentLinkWidth-D9Ej5fM ()F + public final fun getAttachmentsContentMediaGridSpacing-D9Ej5fM ()F + public final fun getAttachmentsContentVideoMaxHeight-D9Ej5fM ()F + public final fun getAttachmentsContentVideoWidth-D9Ej5fM ()F public final fun getChannelAvatarSize-D9Ej5fM ()F public final fun getChannelItemHorizontalPadding-D9Ej5fM ()F public final fun getChannelItemVerticalPadding-D9Ej5fM ()F @@ -1732,6 +1876,7 @@ public final class io/getstream/chat/android/compose/ui/util/ChannelUtilsKt { public final class io/getstream/chat/android/compose/ui/util/ImageUtilsKt { public static final fun mirrorRtl (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/unit/LayoutDirection;)Landroidx/compose/ui/Modifier; + public static final fun rememberStreamImagePainter-MqR-F_0 (Lcoil/request/ImageRequest;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/layout/ContentScale;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter; public static final fun rememberStreamImagePainter-MqR-F_0 (Ljava/lang/Object;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Landroidx/compose/ui/graphics/painter/Painter;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/layout/ContentScale;ILandroidx/compose/runtime/Composer;II)Lcoil/compose/AsyncImagePainter; } @@ -1892,6 +2037,26 @@ public final class io/getstream/chat/android/compose/viewmodel/imagepreview/Imag public fun create (Ljava/lang/Class;)Landroidx/lifecycle/ViewModel; } +public final class io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel : androidx/lifecycle/ViewModel { + public static final field $stable I + public fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Ljava/lang/String;)V + public final fun deleteCurrentMediaAttachment (Lio/getstream/chat/android/client/models/Attachment;)V + public final fun getConnectionState ()Lio/getstream/chat/android/client/models/ConnectionState; + public final fun getMessage ()Lio/getstream/chat/android/client/models/Message; + public final fun getUser ()Lkotlinx/coroutines/flow/StateFlow; + public final fun isShowingGallery ()Z + public final fun isShowingOptions ()Z + public final fun toggleGallery (Z)V + public final fun toggleMediaOptions (Z)V +} + +public final class io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModelFactory : androidx/lifecycle/ViewModelProvider$Factory { + public static final field $stable I + public fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Ljava/lang/String;)V + public synthetic fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun create (Ljava/lang/Class;)Landroidx/lifecycle/ViewModel; +} + public final class io/getstream/chat/android/compose/viewmodel/messages/AttachmentsPickerViewModel : androidx/lifecycle/ViewModel { public static final field $stable I public fun (Lio/getstream/chat/android/compose/ui/util/StorageHelperWrapper;)V diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt index d3a19a918a8..284a005120c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/StreamDimens.kt @@ -89,12 +89,12 @@ import androidx.compose.ui.unit.dp @Immutable public data class StreamDimens @Deprecated( - "This constructor has been deprecated. Parameters 'attachmentsContentImageHeight' and " + - "'attachmentsContentImageGridSpacing' have been deprecated in" + - " favor of 'attachmentsContentImageMaxHeight' and 'attachmentsContentMediaGridSpacing'. " + - "Please use the constructor which does not contain `attachmentsContentImageHeight` and " + + "This constructor has been deprecated. Parameter " + + "'attachmentsContentImageGridSpacing' has been deprecated in " + + "favor of 'attachmentsContentMediaGridSpacing'. " + + "Please use the constructor which does not contain " + "attachmentsContentImageGridSpacing.", - level = DeprecationLevel.ERROR, + level = DeprecationLevel.WARNING, ) constructor( public val channelItemVerticalPadding: Dp, From 521ae4f4c0c59bf19a9ef994c88c5a5245cdd7d6 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Fri, 16 Sep 2022 15:39:13 +0200 Subject: [PATCH 65/69] [3369] Enable sharing videos. --- .../preview/MediaGalleryPreviewActivity.kt | 90 ++++++++++++++----- .../src/main/res/values/strings.xml | 1 + 2 files changed, 70 insertions(+), 21 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 100a6824391..8c40180becc 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -41,6 +41,7 @@ import android.view.Gravity import android.view.ViewGroup import android.widget.FrameLayout import android.widget.MediaController +import android.widget.Toast import android.widget.VideoView import androidx.activity.compose.setContent import androidx.activity.viewModels @@ -983,7 +984,6 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { @Composable private fun MediaGalleryPreviewBottomBar(attachments: List, pagerState: PagerState) { val attachmentCount = attachments.size - val isCurrentAttachmentVideo = attachments[pagerState.currentPage].type == AttachmentType.VIDEO Surface( modifier = Modifier @@ -1007,8 +1007,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { onShareMediaClick(attachments[pagerState.currentPage]) } }, - enabled = mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && - !isCurrentAttachmentVideo + enabled = mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED ) { val shareIcon = if (!isSharingInProgress) { @@ -1020,9 +1019,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { Icon( painter = painterResource(id = shareIcon), contentDescription = stringResource(id = R.string.stream_compose_image_preview_share), - tint = if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && - !isCurrentAttachmentVideo - ) { + tint = if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED) { ChatTheme.colors.textHighEmphasis } else { ChatTheme.colors.disabled @@ -1155,35 +1152,74 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { * @param attachment The attachment to preload and share. */ private fun onShareMediaClick(attachment: Attachment) { - // TODO share videos as well fileSharingJob = lifecycleScope.launch { isSharingInProgress = true - val uri = StreamImageLoader.instance().loadAsBitmap( + + when (attachment.type) { + AttachmentType.IMAGE -> shareImage(attachment) + AttachmentType.VIDEO -> shareVideo(attachment) + else -> toastFailedShare() + } + } + } + + /** + * Fetches an image from Coil's cache and shares it. + * + * @param attachment The attachment used to prepare the URI. + */ + private suspend fun shareImage(attachment: Attachment) { + val attachmentUrl = attachment.imagePreviewUrl + + if (attachmentUrl != null) { + StreamImageLoader.instance().loadAsBitmap( context = applicationContext, - url = attachment.imagePreviewUrl!! + url = attachmentUrl )?.let { - StreamFileUtil.writeImageToSharableFile(applicationContext, it) - } + val imageUri = StreamFileUtil.writeImageToSharableFile(applicationContext, it) - if (uri != null) { - isSharingInProgress = false - shareImage(uri) + shareAttachment( + mediaUri = imageUri, + mediaType = "image/*" + ) } + } else { + toastFailedShare() } } + /** + * Displays a toast saying that sharing the attachment has failed. + */ + private fun toastFailedShare() { + Toast.makeText( + applicationContext, + applicationContext.getString(R.string.stream_compose_media_gallery_preview_could_not_share_attachment), + Toast.LENGTH_SHORT + ).show() + } + /** * Starts a picker to share the current image. * - * @param imageUri The URI of the image to share. + * @param mediaUri The URI of the media attachment to share. + * @param mediaType type of media being shared. */ - private fun shareImage(imageUri: Uri) { + private fun shareAttachment( + mediaUri: Uri?, + mediaType: String, + ) { + if (mediaUri == null) { + toastFailedShare() + return + } + ContextCompat.startActivity( this, Intent.createChooser( Intent(Intent.ACTION_SEND).apply { - type = "image/*" - putExtra(Intent.EXTRA_STREAM, imageUri) + type = mediaType + putExtra(Intent.EXTRA_STREAM, mediaUri) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }, getString(R.string.stream_compose_attachment_gallery_share), @@ -1195,10 +1231,22 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { /** * Starts a picker to share the current image. * - * @param imageUri The URI of the image to share. + * @param attachment The attachment to share. */ - private fun shareVideo(videoUri: Uri) { - // TODO + private suspend fun shareVideo(attachment: Attachment) { + val result = StreamFileUtil.writeFileToShareableFile( + context = applicationContext, + attachment = attachment + ) + + isSharingInProgress = false + + if (result.isSuccess) { + shareAttachment( + mediaUri = result.data(), + mediaType = "video/*" + ) + } } /** diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index ad26b8f8dd5..390d74f4e82 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -168,6 +168,7 @@ Share Photos Preparing... + Could not share attachment Channel item From a6b87239c0042341b01e496c80d7cdf3e894422f Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Sat, 17 Sep 2022 00:09:00 +0200 Subject: [PATCH 66/69] [3369] Enable sharing videos with cache and large file prompts. --- .../api/stream-chat-android-compose.api | 4 + .../preview/MediaGalleryPreviewActivity.kt | 120 +++++++++++++----- .../MediaGalleryPreviewViewModel.kt | 13 ++ .../src/main/res/values/strings.xml | 2 + .../com/getstream/sdk/chat/StreamFileUtil.kt | 59 ++++++++- 5 files changed, 160 insertions(+), 38 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 09f69dd878c..15d1f4da9ca 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -2043,9 +2043,13 @@ public final class io/getstream/chat/android/compose/viewmodel/mediapreview/Medi public final fun deleteCurrentMediaAttachment (Lio/getstream/chat/android/client/models/Attachment;)V public final fun getConnectionState ()Lio/getstream/chat/android/client/models/ConnectionState; public final fun getMessage ()Lio/getstream/chat/android/client/models/Message; + public final fun getPromptedAttachment ()Lio/getstream/chat/android/client/models/Attachment; public final fun getUser ()Lkotlinx/coroutines/flow/StateFlow; + public final fun isSharingInProgress ()Z public final fun isShowingGallery ()Z public final fun isShowingOptions ()Z + public final fun setPromptedAttachment (Lio/getstream/chat/android/client/models/Attachment;)V + public final fun setSharingInProgress (Z)V public final fun toggleGallery (Z)V public final fun toggleMediaOptions (Z)V } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 8c40180becc..00c970758a6 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -150,6 +150,7 @@ import io.getstream.chat.android.compose.ui.attachments.content.PlayButton import io.getstream.chat.android.compose.ui.components.LoadingIndicator import io.getstream.chat.android.compose.ui.components.MediaPreviewPlaceHolder import io.getstream.chat.android.compose.ui.components.NetworkLoadingIndicator +import io.getstream.chat.android.compose.ui.components.SimpleDialog import io.getstream.chat.android.compose.ui.components.Timestamp import io.getstream.chat.android.compose.ui.components.avatar.Avatar import io.getstream.chat.android.compose.ui.theme.ChatTheme @@ -187,11 +188,6 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { */ private var fileSharingJob: Job? = null - /** - * Indicated that we are preparing a file for sharing. - */ - private var isSharingInProgress: Boolean by mutableStateOf(false) - /** * The ViewModel that exposes screen data. */ @@ -265,20 +261,42 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { topBar = { MediaGalleryPreviewTopBar(message) }, content = { contentPadding -> if (message.id.isNotEmpty()) { - - Surface( - modifier = Modifier - .fillMaxSize() - .padding(contentPadding) - ) { - MediaPreviewContent(pagerState, message.attachments) { - coroutineScope.launch { - scaffoldState.snackbarHostState.showSnackbar( - message = getString(R.string.stream_ui_message_list_video_display_error) - ) + Box(Modifier.fillMaxSize()) { + Surface( + modifier = Modifier + .fillMaxSize() + .padding(contentPadding) + ) { + MediaPreviewContent(pagerState, message.attachments) { + coroutineScope.launch { + scaffoldState.snackbarHostState.showSnackbar( + message = getString(R.string.stream_ui_message_list_video_display_error) + ) + } } } } + + val promptedAttachment = mediaGalleryPreviewViewModel.promptedAttachment + + if (promptedAttachment != null) { + SimpleDialog( + title = getString( + R.string.stream_compose_media_gallery_share_large_file_prompt_title, + ), + message = getString( + R.string.stream_compose_media_gallery_share_large_file_prompt_message, + (promptedAttachment.fileSize.toFloat() / (1024 * 1024)) + ), + onPositiveAction = { + shareAttachment(promptedAttachment) + mediaGalleryPreviewViewModel.promptedAttachment = null + }, + onDismiss = { + mediaGalleryPreviewViewModel.promptedAttachment = null + } + ) + } } }, bottomBar = { @@ -1000,17 +1018,35 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { IconButton( modifier = Modifier.align(Alignment.CenterStart), onClick = { - if (isSharingInProgress) { - fileSharingJob?.cancel() - isSharingInProgress = false - } else { - onShareMediaClick(attachments[pagerState.currentPage]) + val attachment = attachments[pagerState.currentPage] + + when { + mediaGalleryPreviewViewModel.isSharingInProgress -> { + fileSharingJob?.cancel() + mediaGalleryPreviewViewModel.isSharingInProgress = false + } + attachment.fileSize >= MaxUnpromptedFileSize -> { + val result = StreamFileUtil.getFileFromCache( + context = applicationContext, + attachment = attachment + ) + + if (result.isSuccess) { + shareAttachment( + mediaUri = result.data(), + attachmentType = attachment.type + ) + } else { + mediaGalleryPreviewViewModel.promptedAttachment = attachment + } + } + else -> shareAttachment(attachment) } }, enabled = mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED ) { - val shareIcon = if (!isSharingInProgress) { + val shareIcon = if (!mediaGalleryPreviewViewModel.isSharingInProgress) { R.drawable.stream_compose_ic_share } else { R.drawable.stream_compose_ic_clear @@ -1031,7 +1067,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { modifier = Modifier.align(Alignment.Center), verticalAlignment = Alignment.CenterVertically ) { - if (isSharingInProgress) { + if (mediaGalleryPreviewViewModel.isSharingInProgress) { CircularProgressIndicator( modifier = Modifier .padding(horizontal = 12.dp) @@ -1041,7 +1077,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { ) } - val text = if (!isSharingInProgress) { + val text = if (!mediaGalleryPreviewViewModel.isSharingInProgress) { stringResource( id = R.string.stream_compose_image_order, pagerState.currentPage + 1, @@ -1147,13 +1183,13 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { } /** - * Handles the logic of loading the image and preparing a shareable file. + * Handles the logic of sharing a file. * - * @param attachment The attachment to preload and share. + * @param attachment The attachment to be shared. */ - private fun onShareMediaClick(attachment: Attachment) { + private fun shareAttachment(attachment: Attachment) { fileSharingJob = lifecycleScope.launch { - isSharingInProgress = true + mediaGalleryPreviewViewModel.isSharingInProgress = true when (attachment.type) { AttachmentType.IMAGE -> shareImage(attachment) @@ -1180,7 +1216,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { shareAttachment( mediaUri = imageUri, - mediaType = "image/*" + attachmentType = attachment.type ) } } else { @@ -1203,17 +1239,26 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { * Starts a picker to share the current image. * * @param mediaUri The URI of the media attachment to share. - * @param mediaType type of media being shared. + * @param attachmentType type of attachment being shared. */ private fun shareAttachment( mediaUri: Uri?, - mediaType: String, + attachmentType: String?, ) { if (mediaUri == null) { toastFailedShare() return } + val mediaType = when (attachmentType) { + AttachmentType.IMAGE -> "image/*" + AttachmentType.VIDEO -> "video/*" + else -> { + toastFailedShare() + return + } + } + ContextCompat.startActivity( this, Intent.createChooser( @@ -1239,12 +1284,12 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { attachment = attachment ) - isSharingInProgress = false + mediaGalleryPreviewViewModel.isSharingInProgress = false if (result.isSuccess) { shareAttachment( mediaUri = result.data(), - mediaType = "video/*" + attachmentType = attachment.type ) } } @@ -1441,6 +1486,15 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { } public companion object { + + /** + * If the file is at least this big or bigger we prompt the user to make sure they + * want to download it. + * + * Expressed in bytes. + */ + private const val MaxUnpromptedFileSize = 10 * 1024 * 1024 + /** * The column count used for the image gallery. */ diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt index a7054ddcbc8..ae1dde7614f 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt @@ -61,6 +61,19 @@ public class MediaGalleryPreviewViewModel( public var message: Message by mutableStateOf(Message()) internal set + /** + * If we are preparing a file for sharing or not. + */ + public var isSharingInProgress: Boolean by mutableStateOf(false) + + /** + * If an attachment needs a prompt to be shared due to a large file size + * this value will be non-null. + * + * You should clear this value once the prompt is removed. + */ + public var promptedAttachment: Attachment? by mutableStateOf(null) + /** * Represent the header title of the gallery screen. */ diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index 390d74f4e82..b185604e4b2 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -169,6 +169,8 @@ Photos Preparing... Could not share attachment + Large file + In order to share it %.2f MB needs to be downloaded. Channel item diff --git a/stream-chat-android-ui-common/src/main/kotlin/com/getstream/sdk/chat/StreamFileUtil.kt b/stream-chat-android-ui-common/src/main/kotlin/com/getstream/sdk/chat/StreamFileUtil.kt index ada4af8411c..d959997da2f 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/com/getstream/sdk/chat/StreamFileUtil.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/com/getstream/sdk/chat/StreamFileUtil.kt @@ -30,7 +30,7 @@ import io.getstream.chat.android.core.internal.InternalStreamChatApi import java.io.File import java.io.FileOutputStream import java.io.IOException -import java.lang.Exception +import kotlin.Exception private const val DEFAULT_BITMAP_QUALITY = 90 @@ -122,6 +122,58 @@ public object StreamFileUtil { } } + /** + * Fetches the given attachment from cache if it has been previously cached. + * Returns an error otherwise. + * + * @param context The Android [Context] used for path resolving and [Uri] fetching. + * @param attachment the attachment to be downloaded. + * + * @return A [Uri] to the file is returned in the form of [Result.data] + * if the file was successfully fetched from the cache. Returns a [ChatError] + * accessible via [Result.error] otherwise. + */ + public fun getFileFromCache( + context: Context, + attachment: Attachment, + ): Result { + return try { + val getOrCreateCacheDirResult = getOrCreateStreamCacheDir(context) + if (getOrCreateCacheDirResult.isError) return Result(error = getOrCreateCacheDirResult.error()) + + val streamCacheDir = getOrCreateCacheDirResult.data() + + val attachmentHashCode = (attachment.url ?: attachment.assetUrl)?.hashCode() + val fileName = CACHED_FILE_PREFIX + attachmentHashCode.toString() + attachment.name + + val file = File(streamCacheDir, fileName) + + // First we check if the file exists. + // We then check the hash code is valid and check file size + // equality to make sure we've completed the download successfully. + val isFileCached = file.exists() && + attachmentHashCode != null && + file.length() == attachment.fileSize.toLong() + + if (isFileCached) { + Result(data = getUriForFile(context, file)) + } else { + Result( + error = ChatError( + message = "No such file in cache.", + ) + ) + } + } catch (e: Exception) { + Result( + error = ChatError( + message = "Cannot determine if the file has been cached.", + cause = e + ) + ) + } + } + /** * Hashes the links of given attachments and then tries to create a new file * under that hash. If the file already exists checks that the full file @@ -151,10 +203,7 @@ public object StreamFileUtil { val file = File(streamCacheDir, fileName) - // When File.createNewFile returns false it means that the file already exists. - // We then check the hash code is valid and check file size - // equality to make sure we've completed the download successfully. - return if (!file.createNewFile() && + return if (file.exists() && attachmentHashCode != null && file.length() == attachment.fileSize.toLong() ) { From 5690ae6855a22f11e51106ed0047739c5464d2f1 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Sat, 17 Sep 2022 00:10:56 +0200 Subject: [PATCH 67/69] [3369] Supress warnings. --- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 1 + .../src/main/kotlin/com/getstream/sdk/chat/StreamFileUtil.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index 00c970758a6..a0c5cede312 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -241,6 +241,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { * @param initialAttachmentPosition The initial pager position, based on the attachment preview * the user clicked on. */ + @Suppress("MagicNumber") @OptIn(ExperimentalAnimationApi::class) @Composable private fun MediaGalleryPreviewContentWrapper( diff --git a/stream-chat-android-ui-common/src/main/kotlin/com/getstream/sdk/chat/StreamFileUtil.kt b/stream-chat-android-ui-common/src/main/kotlin/com/getstream/sdk/chat/StreamFileUtil.kt index d959997da2f..825d208470b 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/com/getstream/sdk/chat/StreamFileUtil.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/com/getstream/sdk/chat/StreamFileUtil.kt @@ -133,6 +133,7 @@ public object StreamFileUtil { * if the file was successfully fetched from the cache. Returns a [ChatError] * accessible via [Result.error] otherwise. */ + @Suppress("TooGenericExceptionCaught") public fun getFileFromCache( context: Context, attachment: Attachment, From 12216d426306be7b5a3bca1b5551c3c19a7f3b78 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Sat, 17 Sep 2022 00:15:26 +0200 Subject: [PATCH 68/69] [3369] Supress warnings. --- .../ui/attachments/preview/MediaGalleryPreviewActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index a0c5cede312..d9a771f2c18 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -241,7 +241,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { * @param initialAttachmentPosition The initial pager position, based on the attachment preview * the user clicked on. */ - @Suppress("MagicNumber") + @Suppress("MagicNumber", "LongMethod") @OptIn(ExperimentalAnimationApi::class) @Composable private fun MediaGalleryPreviewContentWrapper( From fccc566116d251b540abce324f4e8a801c028c36 Mon Sep 17 00:00:00 2001 From: MarinTolic Date: Sat, 17 Sep 2022 01:21:34 +0200 Subject: [PATCH 69/69] [3369] Clear the cache when exiting the gallery and switch to using IO dispatcher for writing files to cache. --- .../preview/MediaGalleryPreviewActivity.kt | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt index d9a771f2c18..a0e5bef8fd2 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivity.kt @@ -158,9 +158,11 @@ import io.getstream.chat.android.compose.ui.util.RetryHash import io.getstream.chat.android.compose.ui.util.rememberStreamImagePainter import io.getstream.chat.android.compose.viewmodel.mediapreview.MediaGalleryPreviewViewModel import io.getstream.chat.android.compose.viewmodel.mediapreview.MediaGalleryPreviewViewModelFactory +import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider import io.getstream.chat.android.uiutils.constant.AttachmentType import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.Date import kotlin.math.abs @@ -1221,6 +1223,7 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { ) } } else { + mediaGalleryPreviewViewModel.isSharingInProgress = false toastFailedShare() } } @@ -1246,6 +1249,8 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { mediaUri: Uri?, attachmentType: String?, ) { + mediaGalleryPreviewViewModel.isSharingInProgress = false + if (mediaUri == null) { toastFailedShare() return @@ -1280,10 +1285,12 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { * @param attachment The attachment to share. */ private suspend fun shareVideo(attachment: Attachment) { - val result = StreamFileUtil.writeFileToShareableFile( - context = applicationContext, - attachment = attachment - ) + val result = withContext(DispatcherProvider.IO) { + StreamFileUtil.writeFileToShareableFile( + context = applicationContext, + attachment = attachment + ) + } mediaGalleryPreviewViewModel.isSharingInProgress = false @@ -1292,6 +1299,8 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { mediaUri = result.data(), attachmentType = attachment.type ) + } else { + toastFailedShare() } } @@ -1486,6 +1495,11 @@ public class MediaGalleryPreviewActivity : AppCompatActivity() { } } + override fun onDestroy() { + super.onDestroy() + StreamFileUtil.clearStreamCache(context = applicationContext) + } + public companion object { /**