diff --git a/CHANGELOG.md b/CHANGELOG.md index c0a053e53ac..fb50ffe8119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,12 +73,28 @@ ### ⬆️ Improved - Improved the way the [ChannelsScreen](https://getstream.io/chat/docs/sdk/android/compose/channel-components/channels-screen/) is built. [#4183](https://github.com/GetStream/stream-chat-android/pull/4183) - Improved the way the [MessagesScreen](https://getstream.io/chat/docs/sdk/android/compose/message-components/messages-screen/) is built. [#4183](https://github.com/GetStream/stream-chat-android/pull/4183) +- 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 - Changed the way ChannelsScreen and MessagesScreen components are built. Instead of exposing a ton of parameters for customization, we now expose a ViewModelFactory that accepts them. [#4183](https://github.com/GetStream/stream-chat-android/pull/4183) - Using this new approach you can reuse and connect to ViewModels from the outside, if you want to power custom behavior. Make sure to check out our documentation regarding these components. [#4183](https://github.com/GetStream/stream-chat-android/pull/4183) +- 🚨 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. +- 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) +- 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 @@ -86,6 +102,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 diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index f73d19dd815..92cbcd746e7 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -4,6 +4,30 @@ 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. | +| `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. | +| `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. | +| `MessageListView::setUserUnmuteHandler` | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | The option to unmute the user from `MessageListView` has been deprecated and will be removed. | +| `MessageListView::setUserMuteHandler` | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | The option to mute the user from `MessageListView` has been deprecated and will be removed. | +| `MessageListView.UserUnmuteHandler` | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | The option to unmute the user from `MessageListView` has been deprecated and will be removed. `UserUnmuteHandler` will be removed with it too. | +| `MessageListView.UserMuteHandler` | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | The option to mute the user from `MessageListView` has been deprecated and will be removed. `UserMuteHandler` will be removed with it too. | +| `MessageListView::setMuteUserEnabled` | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | The option to mute the user from `MessageListView` has been deprecated and will be removed. | +| `MessageListView.UserBlockHandler` | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | The option to block the user from `MessageListView` has been deprecated and will be removed. `UserBlockHandler` will be removed with it too. | +| `MessageListView::setBlockUserEnabled` | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | The option to block the user from `MessageListView` has been deprecated and will be removed. | +| `MessageListView` attributes | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | The attributes `streamUiMuteOptionIcon`, `streamUiUnmuteOptionIcon`, `streamUiMuteUserEnabled`, `streamUiBlockOptionIcon` and `streamUiBlockUserEnabled` have been deprecated and will be removed. The options to block and mute users will no longer be contained inside `MessageListView` | +| `MessageListViewStyle` constructor containing params: `muteIcon`, `unmuteIcon`, `muteEnabled`, `blockIcon` and `blockEnabled` | 2022.08.02
5.7.0 | 2022.09.06 ⌛ | 2022.10.04 ⌛ | This constructor has been deprecated. Use the constructor that does not contain these parameters. | | `StreamDimens` constructor containing parameter `attachmentsContentImageHeight` | 2022.08.16
5.8.0 | 2022.08.30
5.9.0 | 2022.09.13
5.10.0 | 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
5.10.0 | Use `QueryChannelsState.chatEventHandlerFactory` instead. | | Multiple event specific `BaseChatEventHandler` methods | 2022.08.16
5.8.0 | 2022.08.30
5.9.0 | 2022.09.13
5.10.0 | Use `handleChatEvent()` or `handleCidEvent()` instead. | 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-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 a28060acda6..2e228b6c913 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 @@ -132,9 +132,9 @@ class MessagesActivity : BaseConnectedActivity() { 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) { @@ -142,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 0ec51616631..f1c6d85151e 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,16 +1587,17 @@ 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 (JJJJJJJJJJJJJJJJJJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (JJJJJJJJJJJJJJJJJJJJJJJJJJLkotlin/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 @@ -1489,6 +1613,10 @@ 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 component25-0d7_KjU ()J + public final fun component26-0d7_KjU ()J public final fun component3-0d7_KjU ()J public final fun component4-0d7_KjU ()J public final fun component5-0d7_KjU ()J @@ -1496,8 +1624,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-CRp9MJE (JJJJJJJJJJJJJJJJJJJJJJJJJJ)Lio/getstream/chat/android/compose/ui/theme/StreamColors; + public static synthetic fun copy-CRp9MJE$default (Lio/getstream/chat/android/compose/ui/theme/StreamColors;JJJJJJJJJJJJJJJJJJJJJJJJJJILjava/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 @@ -1507,6 +1635,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 @@ -1521,6 +1651,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; } @@ -1532,8 +1664,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 @@ -1574,13 +1708,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 @@ -1588,10 +1727,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 @@ -1737,6 +1881,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; } @@ -1898,6 +2043,30 @@ 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 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 +} + +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/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index bfad63bcfbe..b9068a18e4a 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -2,20 +2,32 @@ + 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() 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 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, ) + 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, ) 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:MessageComposer.kt$( value: String, coolDownTime: Int, attachments: List<Attachment>, validationErrors: List<ValidationError>, ownCapabilities: Set<String>, isInEditMode: Boolean, 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, ) @@ -26,6 +38,13 @@ MagicNumber:ImageAttachmentContent.kt$3 MagicNumber:ImageAttachmentContent.kt$4 MagicNumber:ImagePreviewActivity.kt$ImagePreviewActivity$8f + MagicNumber:MediaAttachmentContent.kt$0.85f + MagicNumber:MediaAttachmentContent.kt$8 + 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 MagicNumber:Messages.kt$3 MagicNumber:Messages.kt$5 MagicNumber:SearchInput.kt$8f @@ -36,5 +55,9 @@ MaxLineLength:MessageOptions.kt$title = if (selectedMessage.pinned) R.string.stream_compose_unpin_message else R.string.stream_compose_pin_message 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 diff --git a/stream-chat-android-compose/src/main/AndroidManifest.xml b/stream-chat-android-compose/src/main/AndroidManifest.xml index e5a7154c8b8..8a72b9d394e 100644 --- a/stream-chat-android-compose/src/main/AndroidManifest.xml +++ b/stream-chat-android-compose/src/main/AndroidManifest.xml @@ -29,5 +29,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/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..c6a6648cbcc --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewActivityAttachmentState.kt @@ -0,0 +1,84 @@ +/* + * 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 io.getstream.chat.android.uiutils.constant.AttachmentType +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. + * 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( + 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/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..38075a2b5d5 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/mediagallerypreview/MediaGalleryPreviewOption.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.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. + * @param isEnabled If the action is currently enabled. + */ +internal data class MediaGalleryPreviewOption( + internal val title: String, + internal val titleColor: Color, + 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/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..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 @@ -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,44 @@ 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 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 = {}, -) + 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/StreamAttachmentFactories.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/StreamAttachmentFactories.kt index 241d53aa629..755067acc62 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 @@ -20,8 +20,8 @@ import androidx.compose.foundation.Image import androidx.compose.ui.layout.ContentScale 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 import io.getstream.chat.android.compose.ui.theme.StreamDimens @@ -69,7 +69,7 @@ public object StreamAttachmentFactories { giphySizingMode = giphySizingMode, contentScale = contentScale, ), - ImageAttachmentFactory(), + MediaAttachmentFactory(), FileAttachmentFactory(), ) 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 ) } 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/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/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..3ef64133153 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentContent.kt @@ -0,0 +1,485 @@ +/* + * 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.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 +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.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 + +/** + * 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 maximumNumberOfPreviewedItems The maximum number of thumbnails that can be displayed + * in a group when previewing Media attachments in the message list. + * @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 +public fun MediaAttachmentContent( + attachmentState: AttachmentState, + modifier: Modifier = Modifier, + maximumNumberOfPreviewedItems: Int = 4, + itemOverlayContent: @Composable (attachmentType: String?) -> Unit = { attachmentType -> + if (attachmentType == AttachmentType.VIDEO) { + PlayButton() + } + }, +) { + val (message, onLongItemClick, _, onMediaGalleryPreviewResult) = attachmentState + val gridSpacing = ChatTheme.dimens.attachmentsContentMediaGridSpacing + + 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() + + SingleMediaAttachment( + attachment = attachment, + message = message, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, + onLongItemClick = onLongItemClick, + overlayContent = itemOverlayContent + ) + } else { + MultipleMediaAttachments( + attachments = attachments, + attachmentCount = attachmentCount, + gridSpacing = gridSpacing, + maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, + message = message, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, + onLongItemClick = onLongItemClick, + itemOverlayContent = itemOverlayContent + ) + } + } +} + +/** + * 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 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 overlayContent Represents the content overlaid above attachment previews. + * Usually used to display a play button over video previews. + */ +@Composable +internal fun SingleMediaAttachment( + attachment: Attachment, + message: Message, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, + onLongItemClick: (Message) -> Unit, + overlayContent: @Composable (attachmentType: String?) -> 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 { + val width = attachment.originalWidth?.toFloat() + val height = attachment.originalHeight?.toFloat() + + if (width != null && height != null) { + width / height + } else { + null + } + } + } + + MediaAttachmentContentItem( + attachment = attachment, + modifier = Modifier + .heightIn( + max = if (isVideo) { + ChatTheme.dimens.attachmentsContentVideoMaxHeight + } else { + ChatTheme.dimens.attachmentsContentImageMaxHeight + } + ) + .width( + if (isVideo) { + ChatTheme.dimens.attachmentsContentVideoWidth + } else { + ChatTheme.dimens.attachmentsContentImageWidth + } + ) + .aspectRatio(ratio ?: EqualDimensionsRatio), + message = message, + attachmentPosition = 0, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, + onLongItemClick = onLongItemClick, + overlayContent = overlayContent + ) +} + +/** + * 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 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. + * @param onLongItemClick Lambda that gets called when an item is long clicked. + * @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.MultipleMediaAttachments( + attachments: List, + attachmentCount: Int, + gridSpacing: Dp, + maximumNumberOfPreviewedItems: Int = 4, + message: Message, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, + onLongItemClick: (Message) -> Unit, + itemOverlayContent: @Composable (attachmentType: String?) -> Unit, +) { + + Column( + modifier = Modifier + .weight(1f, fill = false) + .width(ChatTheme.dimens.attachmentsContentGroupPreviewWidth / 2) + .height(ChatTheme.dimens.attachmentsContentGroupPreviewHeight), + verticalArrangement = Arrangement.spacedBy(gridSpacing) + ) { + for (attachmentIndex in 0 until maximumNumberOfPreviewedItems step 2) { + if (attachmentIndex < attachmentCount) { + MediaAttachmentContentItem( + attachment = attachments[attachmentIndex], + modifier = Modifier.weight(1f), + message = message, + attachmentPosition = attachmentIndex, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, + onLongItemClick = onLongItemClick, + overlayContent = itemOverlayContent + ) + } + } + } + + Column( + modifier = Modifier + .weight(1f, fill = false) + .width(ChatTheme.dimens.attachmentsContentGroupPreviewWidth / 2) + .height(ChatTheme.dimens.attachmentsContentGroupPreviewHeight), + verticalArrangement = Arrangement.spacedBy(gridSpacing) + ) { + 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 == lastItemInColumnIndex && attachmentCount > maximumNumberOfPreviewedItems) { + Box(modifier = Modifier.weight(1f)) { + MediaAttachmentContentItem( + attachment = attachment, + message = message, + attachmentPosition = attachmentIndex, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, + onLongItemClick = onLongItemClick, + overlayContent = itemOverlayContent + ) + + if (!isUploading) { + MediaAttachmentViewMoreOverlay( + mediaCount = attachmentCount, + maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, + modifier = Modifier.align(Alignment.Center) + ) + } + } + } else { + MediaAttachmentContentItem( + attachment = attachment, + modifier = Modifier.weight(1f), + message = message, + attachmentPosition = attachmentIndex, + onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, + onLongItemClick = onLongItemClick, + overlayContent = itemOverlayContent + ) + } + } + } + } +} + +/** + * 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 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 overlayContent Represents the content overlaid above attachment previews. + * Usually used to display a play button over video previews. + */ +@Suppress("LongParameterList", "LongMethod") +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun MediaAttachmentContentItem( + message: Message, + attachmentPosition: Int, + attachment: Attachment, + onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit, + onLongItemClick: (Message) -> Unit, + modifier: Modifier = Modifier, + overlayContent: @Composable (attachmentType: String?) -> 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 + var retryHash by remember { + mutableStateOf(0) + } + + val data = + if (isImage || (isVideo && ChatTheme.videoThumbnailsEnabled)) { + attachment.imagePreviewUrl + } else { + null + } + + val painter = rememberStreamImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(data) + .setParameter(key = RetryHash, 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 (data != null && connectionState == ConnectionState.CONNECTED && + painter.state is AsyncImagePainter.State.Error + ) { + retryHash++ + } + + val areVideosEnabled = ChatTheme.videoThumbnailsEnabled + + Box( + modifier = modifier + .background(Color.Black) + .fillMaxWidth() + .combinedClickable( + interactionSource = MutableInteractionSource(), + indication = rememberRipple(), + onClick = { + mixedMediaPreviewLauncher.launch( + MediaGalleryPreviewContract.Input( + message = message, + initialPosition = attachmentPosition, + videoThumbnailsEnabled = areVideosEnabled + ) + ) + }, + onLongClick = { onLongItemClick(message) } + ), + contentAlignment = Alignment.Center + ) { + val backgroundColor = + if (isImage) ChatTheme.colors.imageBackgroundMessageList + else ChatTheme.colors.videoBackgroundMessageList + + Image( + modifier = modifier + .fillMaxSize() + .background(backgroundColor), + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop + ) + + MediaPreviewPlaceHolder( + asyncImagePainterState = painter.state, + progressIndicatorStrokeWidth = 3.dp, + progressIndicatorFillMaxSizePercentage = 0.25f, + isImage = isImage, + placeholderIconTintColor = ChatTheme.colors.disabled + ) + + if (painter.state !is AsyncImagePainter.State.Loading) { + overlayContent(attachment.type) + } + } +} + +/** + * A simple play button that is overlaid above + * video attachments. + * + * @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, + contentDescription: String? = null, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center + ) { + 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, + ) + } +} + +/** + * Represents an overlay that's shown on the last media attachment preview in the media attachment + * item gallery. + * + * @param mediaCount The number of total media attachments. + * @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, + maximumNumberOfPreviewedItems: Int, + modifier: Modifier = Modifier, +) { + val remainingMediaCount = mediaCount - maximumNumberOfPreviewedItems + + Box( + modifier = Modifier + .fillMaxSize() + .background(color = ChatTheme.colors.overlay), + ) { + Text( + modifier = modifier + .wrapContentSize(), + text = stringResource( + id = R.string.stream_compose_remaining_media_attachments_count, + remainingMediaCount + ), + 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 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..2fdfd8fa0bf --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentPreviewContent.kt @@ -0,0 +1,122 @@ +/* + * 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.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 + +/** + * 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. + * @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, + modifier: Modifier = Modifier, + previewItemOverlayContent: @Composable (attachmentType: String?) -> Unit = { attachmentType -> + if (attachmentType == AttachmentType.VIDEO) { + DefaultPreviewItemOverlayContent() + } + }, +) { + LazyRow( + modifier = modifier.clip(ChatTheme.shapes.attachment), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start) + ) { + items(attachments) { image -> + MediaAttachmentPreviewItem( + mediaAttachment = image, + onAttachmentRemoved = onAttachmentRemoved, + overlayContent = previewItemOverlayContent + ) + } + } +} + +/** + * 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 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, + overlayContent: @Composable (attachmentType: String?) -> 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 + ) + + overlayContent(mediaAttachment.type) + + 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/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..ba19d62e8f2 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MediaAttachmentQuotedContent.kt @@ -0,0 +1,107 @@ +/* + * 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 isImageContent = attachment.type == AttachmentType.IMAGE || attachment.type == AttachmentType.IMGUR + val isVideo = attachment.type == AttachmentType.VIDEO + val isGiphy = attachment.type == AttachmentType.GIPHY + + val backgroundColor = + if (isImageContent || isGiphy) { + ChatTheme.colors.imageBackgroundMessageList + } else { + ChatTheme.colors.videoBackgroundMessageList + } + + val data = + if (isImageContent || isGiphy || (isVideo && ChatTheme.videoThumbnailsEnabled)) { + attachment.imagePreviewUrl + } else { + null + } + + val imagePainter = rememberStreamImagePainter(data = data) + + 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) + .background(backgroundColor), + painter = imagePainter, + contentDescription = null, + contentScale = ContentScale.Crop + ) + + if (isVideo) { + 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/content/MessageAttachmentsContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/MessageAttachmentsContent.kt index cb170906600..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 @@ -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,14 @@ 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 +60,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/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/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..92ec7366e88 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/factory/MediaAttachmentFactory.kt @@ -0,0 +1,117 @@ +/* + * 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.background +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.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.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 +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.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. Values between 4 and 8 are optimal. + * @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, + itemOverlayContent: @Composable (attachmentType: String?) -> Unit = { attachmentType -> + if (attachmentType == AttachmentType.VIDEO) { + DefaultItemOverlayContent() + } + }, + previewItemOverlayContent: @Composable (attachmentType: String?) -> Unit = { attachmentType -> + if (attachmentType == AttachmentType.VIDEO) { + DefaultPreviewItemOverlayContent() + } + }, +): AttachmentFactory = + AttachmentFactory( + canHandle = { + it.none { attachment -> + attachment.type != AttachmentType.IMAGE && attachment.type != AttachmentType.VIDEO + } + }, + previewContent = { modifier, attachments, onAttachmentRemoved -> + MediaAttachmentPreviewContent( + attachments = attachments, + onAttachmentRemoved = onAttachmentRemoved, + modifier = modifier, + previewItemOverlayContent = previewItemOverlayContent + ) + }, + content = @Composable { modifier, state -> + MediaAttachmentContent( + modifier = modifier, + attachmentState = state, + maximumNumberOfPreviewedItems = maximumNumberOfPreviewedItems, + itemOverlayContent = itemOverlayContent + ) + } + ) + +/** + * Represents the default play button that is + * overlaid above video attachment previews inside + * the messages list. + */ +@Preview(name = "DefaultItemOverlayContent Preview") +@Composable +private fun DefaultItemOverlayContent() { + PlayButton( + modifier = Modifier + .padding(2.dp) + .shadow(6.dp, shape = CircleShape) + .background(color = Color.White, shape = CircleShape) + .fillMaxWidth(0.25f) + .aspectRatio(1f) + ) +} + +/** + * Represents the default play button that is + * overlaid above video attachment previews inside + * the message input. + */ +@Preview(name = "DefaultPreviewItemOverlayContent Preview") +@Composable +internal fun DefaultPreviewItemOverlayContent() { + PlayButton( + modifier = Modifier + .shadow(6.dp, shape = CircleShape) + .background(color = Color.White, shape = CircleShape) + .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..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,13 +19,14 @@ 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 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. */ @@ -40,11 +41,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/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( + KeyMediaGalleryPreviewActivityState + )?.messageId ?: "" + ) + } + + /** + * Holds a job used to share an image or a file. + */ + private var fileSharingJob: Job? = null + + /** + * 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 mediaGalleryPreviewActivityState = intent?.getParcelableExtra( + KeyMediaGalleryPreviewActivityState + ) + val videoThumbnailsEnabled = intent?.getBooleanExtra(KeyVideoThumbnailsEnabled, true) ?: true + 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()) { + throw IllegalArgumentException("Missing messageId necessary to load images.") + } + + setContent { + ChatTheme(videoThumbnailsEnabled = videoThumbnailsEnabled) { + val message = mediaGalleryPreviewViewModel.message + + if (message.deletedAt != null) { + finish() + return@ChatTheme + } + + 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. + */ + @Suppress("MagicNumber", "LongMethod") + @OptIn(ExperimentalAnimationApi::class) + @Composable + private fun MediaGalleryPreviewContentWrapper( + message: Message, + initialAttachmentPosition: Int, + ) { + 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()) { + 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 = { + if (message.id.isNotEmpty()) { + 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() + ) + ) + } + + 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 }) + ) + ) + } + } + } + } + + /** + * 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), + message = message + ) + } + } + } + + /** + * 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, + ) { + 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()) + } + } + + /** + * Toggles the media attachments options menu. + * + * @param modifier Modifier for styling. + */ + @Composable + private fun MediaGalleryPreviewOptionsToggle( + message: Message, + modifier: Modifier = Modifier, + ) { + Icon( + modifier = modifier + .size(24.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + 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 = if (message.id.isNotEmpty()) ChatTheme.colors.textHighEmphasis else ChatTheme.colors.disabled + ) + } + + /** + * 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) + .padding(8.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = { + mediaGalleryPreviewViewModel.toggleMediaOptions(isShowingOptions = false) + handleMediaAction( + mediaGalleryPreviewOption.action, + pagerState.currentPage, + downloadPermissionHandler + ) + }, + enabled = mediaGalleryPreviewOption.isEnabled + ), + 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, + onPlaybackError: () -> Unit, + ) { + 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 = onPlaybackError + ) + } + } + } + + /** + * 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 + ) { + + // 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 data = attachment.imagePreviewUrl + val painter = + rememberStreamImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(data) + .crossfade(true) + .setParameter(key = RetryHash, value = retryHash) + .build() + ) + + 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) + + // Used to refresh the request for the current page + // if it has previously failed. + if (data != null && 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) + .background(color = ChatTheme.colors.overlay) + } else { + Modifier + } + + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + MediaPreviewPlaceHolder( + asyncImagePainterState = painter.state, + isImage = attachment.type == AttachmentType.IMAGE, + progressIndicatorStrokeWidth = 6.dp, + progressIndicatorFillMaxSizePercentage = 0.2f + ) + + 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 has finished downloading. + if (!hasPrepared && userHasClickedPlay && page == pagerState.currentPage) { + 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 data = if (ChatTheme.videoThumbnailsEnabled) { + attachment.thumbUrl + } else { + null + } + + val painter = rememberStreamImagePainter(data = data) + + 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(6.dp, shape = CircleShape) + .background(color = Color.White, shape = CircleShape) + .size( + width = 42.dp, + height = 42.dp + ), + contentDescription = getString(R.string.stream_compose_cd_play_button) + ) + } + } + } + + 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. + */ + @Suppress("LongMethod") + @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 = { + 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 (!mediaGalleryPreviewViewModel.isSharingInProgress) { + R.drawable.stream_compose_ic_share + } else { + R.drawable.stream_compose_ic_clear + } + + Icon( + painter = painterResource(id = shareIcon), + contentDescription = stringResource(id = R.string.stream_compose_image_preview_share), + tint = if (mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED) { + ChatTheme.colors.textHighEmphasis + } else { + ChatTheme.colors.disabled + }, + ) + } + + Row( + modifier = Modifier.align(Alignment.Center), + verticalAlignment = Alignment.CenterVertically + ) { + if (mediaGalleryPreviewViewModel.isSharingInProgress) { + CircularProgressIndicator( + modifier = Modifier + .padding(horizontal = 12.dp) + .size(24.dp), + strokeWidth = 2.dp, + color = ChatTheme.colors.primaryAccent, + ) + } + + val text = if (!mediaGalleryPreviewViewModel.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), + 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 isChatConnected by remember(mediaGalleryPreviewViewModel.connectionState) { + derivedStateOf { + mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED + } + } + + val saveMediaColor = + if (isChatConnected) { + ChatTheme.colors.textHighEmphasis + } else { + ChatTheme.colors.disabled + } + + 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), + 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), + isEnabled = true + ), + MediaGalleryPreviewOption( + title = stringResource(id = R.string.stream_compose_media_gallery_preview_save_image), + titleColor = saveMediaColor, + iconPainter = painterResource(id = R.drawable.stream_compose_ic_download), + iconColor = saveMediaColor, + action = SaveMedia(message), + isEnabled = isChatConnected + ) + ) + + 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 = deleteColor, + iconPainter = painterResource(id = R.drawable.stream_compose_ic_delete), + iconColor = deleteColor, + action = Delete(message), + isEnabled = isChatConnected + ) + ) + } + + return options + } + + /** + * Handles the logic of sharing a file. + * + * @param attachment The attachment to be shared. + */ + private fun shareAttachment(attachment: Attachment) { + fileSharingJob = lifecycleScope.launch { + mediaGalleryPreviewViewModel.isSharingInProgress = true + + 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 = attachmentUrl + )?.let { + val imageUri = StreamFileUtil.writeImageToSharableFile(applicationContext, it) + + shareAttachment( + mediaUri = imageUri, + attachmentType = attachment.type + ) + } + } else { + mediaGalleryPreviewViewModel.isSharingInProgress = false + 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 mediaUri The URI of the media attachment to share. + * @param attachmentType type of attachment being shared. + */ + private fun shareAttachment( + mediaUri: Uri?, + attachmentType: String?, + ) { + mediaGalleryPreviewViewModel.isSharingInProgress = false + + if (mediaUri == null) { + toastFailedShare() + return + } + + val mediaType = when (attachmentType) { + AttachmentType.IMAGE -> "image/*" + AttachmentType.VIDEO -> "video/*" + else -> { + toastFailedShare() + return + } + } + + ContextCompat.startActivity( + this, + Intent.createChooser( + Intent(Intent.ACTION_SEND).apply { + type = mediaType + putExtra(Intent.EXTRA_STREAM, mediaUri) + 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 attachment The attachment to share. + */ + private suspend fun shareVideo(attachment: Attachment) { + val result = withContext(DispatcherProvider.IO) { + StreamFileUtil.writeFileToShareableFile( + context = applicationContext, + attachment = attachment + ) + } + + mediaGalleryPreviewViewModel.isSharingInProgress = false + + if (result.isSuccess) { + shareAttachment( + mediaUri = result.data(), + attachmentType = attachment.type + ) + } else { + toastFailedShare() + } + } + + /** + * 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 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 { + mutableStateOf(0) + } + + val coroutineScope = rememberCoroutineScope() + + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .clickable { + coroutineScope.launch { + mediaGalleryPreviewViewModel.toggleGallery(isShowingGallery = false) + pagerState.animateScrollToPage(index) + } + }, + contentAlignment = Alignment.Center + ) { + val data = + if (isImage || (isVideo && ChatTheme.videoThumbnailsEnabled)) { + attachment.imagePreviewUrl + } else { + null + } + + val painter = rememberStreamImagePainter( + model = ImageRequest.Builder(LocalContext.current) + .data(data) + .setParameter(RetryHash, retryHash.toString()) + .build() + ) + + if (data != null && mediaGalleryPreviewViewModel.connectionState == ConnectionState.CONNECTED && + painter.state is AsyncImagePainter.State.Error + ) { + retryHash++ + } + + val backgroundColor = if (isImage) ChatTheme.colors.imageBackgroundMediaGalleryPicker + else ChatTheme.colors.videoBackgroundMediaGalleryPicker + + Image( + modifier = Modifier + .padding(1.dp) + .fillMaxSize() + .background(color = backgroundColor), + painter = painter, + contentDescription = null, + contentScale = ContentScale.Crop + ) + + MediaPreviewPlaceHolder( + asyncImagePainterState = painter.state, + isImage = isImage, + progressIndicatorStrokeWidth = 3.dp, + progressIndicatorFillMaxSizePercentage = 0.3f + ) + + Avatar( + modifier = Modifier + .align(Alignment.TopStart) + .padding(8.dp) + .size(24.dp) + .border( + width = 1.dp, + color = Color.White, + shape = ChatTheme.shapes.avatar + ) + .shadow( + elevation = 4.dp, + shape = ChatTheme.shapes.avatar + ), + imageUrl = user.image, + initials = user.initials + ) + + 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), + contentDescription = getString(R.string.stream_compose_cd_play_button) + ) + } + } + } + + override fun onDestroy() { + super.onDestroy() + StreamFileUtil.clearStreamCache(context = applicationContext) + } + + 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. + */ + private const val ColumnCount = 3 + + /** + * Represents the key for the ID of the message with the attachments we're browsing. + */ + 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. + */ + 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 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, + 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 new file mode 100644 index 00000000000..4c2ce4b5ace --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewContract.kt @@ -0,0 +1,67 @@ +/* + * 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.client.models.Message +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, + message = input.message, + attachmentPosition = input.initialPosition, + videoThumbnailsEnabled = input.videoThumbnailsEnabled + ) + } + + /** + * 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 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/components/MediaPreviewPlaceHolder.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/MediaPreviewPlaceHolder.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/MediaPreviewPlaceHolder.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 + ) + } + } +} 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 c655ecebadd..f3ff50c8073 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 @@ -105,7 +105,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 7676dbf7e48..4d0907490f8 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 @@ -60,7 +60,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 @@ -181,9 +181,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) { @@ -191,7 +191,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/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 } 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 f14e14dab45..cfb820740bf 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 @@ -47,6 +47,14 @@ import io.getstream.chat.android.compose.R * @param threadSeparatorGradientEnd Used as an end color for vertical gradient background in a thread separator. * @param ownMessageText Used for message text color for the current user. [textHighEmphasis] by default. * @param otherMessageText Used for message text color for other users. [textHighEmphasis] by default. + * @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. + * @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( @@ -71,7 +79,11 @@ public data class StreamColors( public val threadSeparatorGradientStart: Color, public val threadSeparatorGradientEnd: Color, public val ownMessageText: Color = textHighEmphasis, - public val otherMessageText: Color = textHighEmphasis + public val otherMessageText: Color = textHighEmphasis, + public val imageBackgroundMessageList: Color, + public val imageBackgroundMediaGalleryPicker: Color, + public val videoBackgroundMessageList: Color, + public val videoBackgroundMediaGalleryPicker: Color, ) { public companion object { @@ -103,7 +115,11 @@ public data class StreamColors( threadSeparatorGradientStart = colorResource(R.color.stream_compose_input_background), threadSeparatorGradientEnd = colorResource(R.color.stream_compose_app_background), ownMessageText = colorResource(R.color.stream_compose_text_high_emphasis), - otherMessageText = colorResource(R.color.stream_compose_text_high_emphasis) + otherMessageText = colorResource(R.color.stream_compose_text_high_emphasis), + 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), ) /** @@ -134,7 +150,11 @@ public data class StreamColors( threadSeparatorGradientStart = colorResource(R.color.stream_compose_input_background_dark), threadSeparatorGradientEnd = colorResource(R.color.stream_compose_app_background_dark), ownMessageText = colorResource(R.color.stream_compose_text_high_emphasis_dark), - otherMessageText = colorResource(R.color.stream_compose_text_high_emphasis_dark) + otherMessageText = colorResource(R.color.stream_compose_text_high_emphasis_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), ) } } 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 5bba43e6088..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 @@ -76,9 +76,27 @@ import androidx.compose.ui.unit.dp * re-sizing itself in order to follow its aspect ratio. * @param attachmentsContentGiphyMaxHeight The maximum height a Giphy attachment will expand to while automatically * re-sizing itself in order to follow 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 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( +public data class StreamDimens +@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.WARNING, +) +constructor( public val channelItemVerticalPadding: Dp, public val channelItemHorizontalPadding: Dp, public val channelAvatarSize: Dp, @@ -124,8 +142,175 @@ public data class StreamDimens( public val attachmentsContentImageMaxHeight: Dp, public val attachmentsContentGiphyMaxWidth: Dp = attachmentsContentGiphyWidth, public val attachmentsContentGiphyMaxHeight: Dp = attachmentsContentGiphyHeight, + public val attachmentsContentVideoMaxHeight: Dp, + public val attachmentsContentMediaGridSpacing: Dp, + public val attachmentsContentVideoWidth: Dp, + public val attachmentsContentGroupPreviewWidth: Dp, + public val attachmentsContentGroupPreviewHeight: 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, + attachmentsContentGiphyMaxWidth: Dp = attachmentsContentGiphyWidth, + attachmentsContentGiphyMaxHeight: Dp = attachmentsContentGiphyHeight, + 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, + 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, + attachmentsContentGiphyMaxWidth = attachmentsContentGiphyMaxWidth, + attachmentsContentGiphyMaxHeight = attachmentsContentGiphyMaxHeight, + attachmentsContentVideoMaxHeight = attachmentsContentVideoMaxHeight, + attachmentsContentMediaGridSpacing = attachmentsContentMediaGridSpacing, + attachmentsContentVideoWidth = attachmentsContentVideoWidth, + attachmentsContentGroupPreviewWidth = attachmentsContentGroupPreviewWidth, + attachmentsContentGroupPreviewHeight = attachmentsContentGroupPreviewHeight + ) + public companion object { /** * Builds the default dimensions for our theme. @@ -140,7 +325,6 @@ public data class StreamDimens( selectedChannelMenuUserItemHorizontalPadding = 8.dp, selectedChannelMenuUserItemAvatarSize = 64.dp, attachmentsContentImageWidth = 250.dp, - attachmentsContentImageGridSpacing = 2.dp, attachmentsContentGiphyWidth = 250.dp, attachmentsContentGiphyHeight = 200.dp, attachmentsContentLinkWidth = 250.dp, @@ -176,6 +360,11 @@ public data class StreamDimens( groupAvatarInitialsXOffset = 1.5.dp, groupAvatarInitialsYOffset = 2.5.dp, attachmentsContentImageMaxHeight = 600.dp, + attachmentsContentVideoMaxHeight = 400.dp, + attachmentsContentMediaGridSpacing = 2.dp, + attachmentsContentVideoWidth = 250.dp, + attachmentsContentGroupPreviewWidth = 250.dp, + attachmentsContentGroupPreviewHeight = 250.dp, ) } } 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..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 @@ -115,3 +115,53 @@ 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, + ) +} + +/** + * 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 RetryHash: String = "retry_hash" 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..ae1dde7614f --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModel.kt @@ -0,0 +1,186 @@ +/* + * 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 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 kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +/** + * A ViewModel capable of loading images, playing videos. + */ +public class MediaGalleryPreviewViewModel( + private val chatClient: ChatClient, + private val clientState: ClientState, + private val messageId: String, +) : ViewModel() { + + /** + * The currently logged in user. + */ + 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()) + 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. + */ + public var connectionState: ConnectionState by mutableStateOf(ConnectionState.OFFLINE) + 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 { + viewModelScope.launch { + 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() + hasCompleteMessage = true + } + } + + /** + * Attempts to fetch the message again if it was not + * successfully fetched the previous time + */ + private suspend fun observeConnectionStateChanges() { + clientState.connectionState.collect { connectionState -> + when (connectionState) { + ConnectionState.CONNECTED -> { + onConnected() + this.connectionState = connectionState + } + ConnectionState.CONNECTING -> this.connectionState = connectionState + ConnectionState.OFFLINE -> this.connectionState = connectionState + } + } + } + + private suspend fun onConnected() { + if (message.id.isEmpty() || !hasCompleteMessage) { + fetchMessage() + } + } + + /** + * 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.url == currentMediaAttachment.url + } + + 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..8ca32e4f296 --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/mediapreview/MediaGalleryPreviewViewModelFactory.kt @@ -0,0 +1,51 @@ +/* + * 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 +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 { + + /** + * Creates a new instance of [MediaGalleryPreviewViewModel] class. + */ + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return MediaGalleryPreviewViewModel( + chatClient = chatClient, + clientState = clientState, + messageId = messageId + ) as T + } +} 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 @@ + + + + + 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..b185604e4b2 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,9 +159,23 @@ Share Photos + + Media options + Reply + Show in chat + Save media + Delete + Share + Photos + Preparing... + Could not share attachment + Large file + In order to share it %.2f MB needs to be downloaded. + Channel item Message item Message input Send button + Play button 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 -> }, ) } } 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..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 @@ -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,59 @@ 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. + */ + @Suppress("TooGenericExceptionCaught") + 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 +204,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() ) { 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