diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f0d38fdad1..374f0d44f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ - Enable pagination in `MentionListView`. [#5692](https://github.com/GetStream/stream-chat-android/pull/5692) ### ✅ Added +- Add `ChatUI.draftMessagesEnabled` property to enable/disable Draft Messages. [#5687](https://github.com/GetStream/stream-chat-android/pull/5687) ### ⚠️ Changed - 🚨Breaking change: Move `MentionListViewModel` logic and its state to a shared component so they can be reused in Compose. [#5692](https://github.com/GetStream/stream-chat-android/pull/5692) @@ -82,6 +83,8 @@ ### ⬆️ Improved ### ✅ Added +- Add `MessagesViewModelFactory.isComposerDraftMessageEnabled` property to enable/disable Draft Messages within `MessageComposer`. [#5687](https://github.com/GetStream/stream-chat-android/pull/5687) +- Add `ChannelViewModelFactory.isDraftMessageEnabled` property to enable/disable Draft Messages within `ChannelList`. [#5687](https://github.com/GetStream/stream-chat-android/pull/5687) ### ⚠️ Changed - `defaultMessageOptionsState()` now accepts an `isInThread` flag to show/hide the "Thread reply" option. [#5683](https://github.com/GetStream/stream-chat-android/pull/5683) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt index b9a052449cd..11745e6e921 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt @@ -1916,6 +1916,16 @@ internal constructor( } } + /** + * Create a new draft message. + * The call will be retried accordingly to [retryPolicy]. + * + * @param channelType The channel type. ie messaging. + * @param channelId The channel id. ie 123. + * @param message The draft message to create. + * + * @return Executable async [Call] responsible for creating a draft message. + */ @CheckResult public fun createDraftMessage( channelType: String, @@ -1935,6 +1945,16 @@ internal constructor( } } + /** + * Delete a draft message. + * The call will be retried accordingly to [retryPolicy]. + * + * @param channelType The channel type. ie messaging. + * @param channelId The channel id. ie 123. + * @param message The draft message to delete. + * + * @return Executable async [Call] responsible for deleting a draft message. + */ @CheckResult public fun deleteDraftMessages( channelType: String, diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt index 53da86dfbb7..0ae0910f2d2 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/DomainMapping.kt @@ -214,7 +214,7 @@ internal class DomainMapping( attachments = message.attachments?.map { it.toDomain() } ?: emptyList(), cid = channel_cid, id = message.id, - parentId = parent_message?.id, + parentId = parent_message?.id ?: parent_id, replyMessage = quoted_message?.toDomain(), showInChannel = message.show_in_channel, mentionedUsersIds = message.mentioned_users?.map { it.id } ?: emptyList(), diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/MessageDtos.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/MessageDtos.kt index f61d5e1543b..d945b67462d 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/MessageDtos.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/MessageDtos.kt @@ -107,6 +107,7 @@ internal data class DownstreamDraftDto( val message: DownstreamDraftMessageDto, val channel_cid: String, val quoted_message: DownstreamMessageDto? = null, + val parent_id: String? = null, val parent_message: DownstreamMessageDto? = null, ) diff --git a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt index 7cbf1803044..4f6e9f6baee 100644 --- a/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt +++ b/stream-chat-android-compose-sample/src/main/java/io/getstream/chat/android/compose/sample/feature/channel/list/ChannelsActivity.kt @@ -101,6 +101,7 @@ class ChannelsActivity : BaseConnectedActivity() { Filters.or(Filters.notExists(CHANNEL_ARG_DRAFT), Filters.eq(CHANNEL_ARG_DRAFT, false)), ), chatEventHandlerFactory = CustomChatEventHandlerFactory(), + isDraftMessageEnabled = true, ) } private val threadsViewModelFactory by lazy { ThreadsViewModelFactory() } 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 2196e803a7c..8393ee9bd49 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 @@ -110,6 +110,7 @@ class MessagesActivity : BaseConnectedActivity() { deletedMessageVisibility = DeletedMessageVisibility.ALWAYS_VISIBLE, messageId = intent.getStringExtra(KEY_MESSAGE_ID), parentMessageId = intent.getStringExtra(KEY_PARENT_MESSAGE_ID), + isComposerDraftMessageEnabled = true, ) } 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 8f6fa6c685a..8599d44b877 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -86,15 +86,17 @@ public abstract class io/getstream/chat/android/compose/state/channels/list/Item public final class io/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState : io/getstream/chat/android/compose/state/channels/list/ItemState { public static final field $stable I - public fun (Lio/getstream/chat/android/models/Channel;ZLjava/util/List;)V - public synthetic fun (Lio/getstream/chat/android/models/Channel;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/models/Channel;ZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;)V + public synthetic fun (Lio/getstream/chat/android/models/Channel;ZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/models/Channel; public final fun component2 ()Z public final fun component3 ()Ljava/util/List; - public final fun copy (Lio/getstream/chat/android/models/Channel;ZLjava/util/List;)Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState; - public static synthetic fun copy$default (Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState;Lio/getstream/chat/android/models/Channel;ZLjava/util/List;ILjava/lang/Object;)Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState; + public final fun component4 ()Lio/getstream/chat/android/models/DraftMessage; + public final fun copy (Lio/getstream/chat/android/models/Channel;ZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;)Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState; + public static synthetic fun copy$default (Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState;Lio/getstream/chat/android/models/Channel;ZLjava/util/List;Lio/getstream/chat/android/models/DraftMessage;ILjava/lang/Object;)Lio/getstream/chat/android/compose/state/channels/list/ItemState$ChannelItemState; public fun equals (Ljava/lang/Object;)Z public final fun getChannel ()Lio/getstream/chat/android/models/Channel; + public final fun getDraftMessage ()Lio/getstream/chat/android/models/DraftMessage; public fun getKey ()Ljava/lang/String; public final fun getTypingUsers ()Ljava/util/List; public fun hashCode ()I @@ -3715,12 +3717,13 @@ public final class io/getstream/chat/android/compose/ui/util/MessageListUtilsKt public abstract interface class io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter { public static final field Companion Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter$Companion; + public abstract fun formatDraftMessagePreview (Lio/getstream/chat/android/models/DraftMessage;)Landroidx/compose/ui/text/AnnotatedString; public abstract fun formatMessagePreview (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;)Landroidx/compose/ui/text/AnnotatedString; public abstract fun formatMessageTitle (Lio/getstream/chat/android/models/Message;)Landroidx/compose/ui/text/AnnotatedString; } public final class io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter$Companion { - public final fun defaultFormatter (Landroid/content/Context;ZLio/getstream/chat/android/compose/ui/theme/StreamTypography;Ljava/util/List;)Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter; + public final fun defaultFormatter (Landroid/content/Context;ZLio/getstream/chat/android/compose/ui/theme/StreamTypography;Ljava/util/List;Lio/getstream/chat/android/compose/ui/theme/StreamColors;)Lio/getstream/chat/android/compose/ui/util/MessagePreviewFormatter; } public abstract interface class io/getstream/chat/android/compose/ui/util/MessagePreviewIconFactory { @@ -3911,8 +3914,8 @@ public final class io/getstream/chat/android/compose/util/KeyValuePair { public final class io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel : androidx/lifecycle/ViewModel { public static final field $stable I - public fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/querysort/QuerySorter;Lio/getstream/chat/android/models/FilterObject;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;J)V - public synthetic fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/querysort/QuerySorter;Lio/getstream/chat/android/models/FilterObject;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;JILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/querysort/QuerySorter;Lio/getstream/chat/android/models/FilterObject;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;JZ)V + public synthetic fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/querysort/QuerySorter;Lio/getstream/chat/android/models/FilterObject;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;JZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun archiveChannel (Lio/getstream/chat/android/models/Channel;)V public final fun deleteConversation (Lio/getstream/chat/android/models/Channel;)V public final fun dismissChannelAction ()V @@ -3944,8 +3947,8 @@ public final class io/getstream/chat/android/compose/viewmodel/channels/ChannelL public final class io/getstream/chat/android/compose/viewmodel/channels/ChannelViewModelFactory : androidx/lifecycle/ViewModelProvider$Factory { public static final field $stable I public fun ()V - public fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/querysort/QuerySorter;Lio/getstream/chat/android/models/FilterObject;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;)V - public synthetic fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/querysort/QuerySorter;Lio/getstream/chat/android/models/FilterObject;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/querysort/QuerySorter;Lio/getstream/chat/android/models/FilterObject;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;Z)V + public synthetic fun (Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/models/querysort/QuerySorter;Lio/getstream/chat/android/models/FilterObject;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun create (Ljava/lang/Class;)Landroidx/lifecycle/ViewModel; } @@ -4134,8 +4137,8 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageL public final class io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory : androidx/lifecycle/ViewModelProvider$Factory { public static final field $stable I - public fun (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/UserLookupHandler;Lkotlin/jvm/functions/Function1;ILio/getstream/chat/android/ui/common/helper/ClipboardHandler;ZIZLio/getstream/chat/android/ui/common/state/messages/list/DeletedMessageVisibility;Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/MessagePositionHandler;ZZZZ)V - public synthetic fun (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/UserLookupHandler;Lkotlin/jvm/functions/Function1;ILio/getstream/chat/android/ui/common/helper/ClipboardHandler;ZIZLio/getstream/chat/android/ui/common/state/messages/list/DeletedMessageVisibility;Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/MessagePositionHandler;ZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/UserLookupHandler;Lkotlin/jvm/functions/Function1;ILio/getstream/chat/android/ui/common/helper/ClipboardHandler;ZIZLio/getstream/chat/android/ui/common/state/messages/list/DeletedMessageVisibility;Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/MessagePositionHandler;ZZZZZ)V + public synthetic fun (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/UserLookupHandler;Lkotlin/jvm/functions/Function1;ILio/getstream/chat/android/ui/common/helper/ClipboardHandler;ZIZLio/getstream/chat/android/ui/common/state/messages/list/DeletedMessageVisibility;Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/MessagePositionHandler;ZZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun create (Ljava/lang/Class;)Landroidx/lifecycle/ViewModel; } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ItemState.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ItemState.kt index dc672ffc530..c6b19329059 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ItemState.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/state/channels/list/ItemState.kt @@ -17,6 +17,7 @@ package io.getstream.chat.android.compose.state.channels.list import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User @@ -32,11 +33,13 @@ public sealed class ItemState { * @param channel The channel to show. * @param isMuted If the channel is muted for the current user. * @param typingUsers The list of users currently typing in the channel. + * @param draftMessage The draft message for the current user in the channel. */ public data class ChannelItemState( val channel: Channel, val isMuted: Boolean = false, val typingUsers: List = emptyList(), + val draftMessage: DraftMessage? = null, ) : ItemState() { override val key: String = channel.cid } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt index 6ec81ddbc52..33dff7397c6 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelItem.kt @@ -57,6 +57,7 @@ import io.getstream.chat.android.compose.ui.theme.ChatPreviewTheme import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.getLastMessage import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.User import io.getstream.chat.android.previewdata.PreviewChannelData import io.getstream.chat.android.previewdata.PreviewUserData @@ -216,9 +217,13 @@ internal fun RowScope.DefaultChannelItemCenterContent( if (channelItemState.typingUsers.isNotEmpty()) { UserTypingIndicator(channelItemState.typingUsers) } else { - val lastMessageText = channelItemState.channel.getLastMessage(currentUser)?.let { lastMessage -> - ChatTheme.messagePreviewFormatter.formatMessagePreview(lastMessage, currentUser) - } ?: AnnotatedString("") + val lastMessageText = + channelItemState.draftMessage + ?.let { ChatTheme.messagePreviewFormatter.formatDraftMessagePreview(it) } + ?: channelItemState.channel.getLastMessage(currentUser)?.let { lastMessage -> + ChatTheme.messagePreviewFormatter.formatMessagePreview(lastMessage, currentUser) + } + ?: AnnotatedString("") if (lastMessageText.isNotEmpty()) { Text( @@ -377,6 +382,7 @@ private fun ChannelItemPreview( channel: Channel, isMuted: Boolean = false, currentUser: User? = null, + draftMessage: DraftMessage? = null, ) { ChatPreviewTheme { ChannelItem( @@ -384,6 +390,7 @@ private fun ChannelItemPreview( channel = channel, isMuted = isMuted, typingUsers = emptyList(), + draftMessage = draftMessage, ), currentUser = currentUser, onChannelClick = {}, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt index 0d4048a536f..c413396a825 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/list/ChannelList.kt @@ -51,6 +51,7 @@ import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User import io.getstream.chat.android.models.querysort.QuerySortByField import io.getstream.chat.android.previewdata.PreviewChannelData +import io.getstream.chat.android.previewdata.PreviewMessageData import io.getstream.chat.android.previewdata.PreviewUserData /** @@ -379,22 +380,32 @@ private fun ChannelListForContentStatePreview() { ItemState.ChannelItemState( channel = PreviewChannelData.channelWithImage, typingUsers = emptyList(), + draftMessage = null, ), ItemState.ChannelItemState( channel = PreviewChannelData.channelWithMessages, typingUsers = emptyList(), + draftMessage = null, ), ItemState.ChannelItemState( channel = PreviewChannelData.channelWithFewMembers, typingUsers = emptyList(), + draftMessage = null, ), ItemState.ChannelItemState( channel = PreviewChannelData.channelWithManyMembers, typingUsers = emptyList(), + draftMessage = null, ), ItemState.ChannelItemState( channel = PreviewChannelData.channelWithOnlineUser, typingUsers = emptyList(), + draftMessage = null, + ), + ItemState.ChannelItemState( + channel = PreviewChannelData.channelWithOnlineUser, + typingUsers = emptyList(), + draftMessage = PreviewMessageData.draftMessage, ), ), ), 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 fb4a683ac26..d900f8f4990 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 @@ -331,6 +331,7 @@ public fun ChatTheme( typography = typography, attachmentFactories = attachmentFactories, autoTranslationEnabled = autoTranslationEnabled, + colors = colors, ), searchResultNameFormatter: SearchResultNameFormatter = SearchResultNameFormatter.defaultFormatter(), imageLoaderFactory: StreamCoilImageLoaderFactory = StreamCoilImageLoaderFactory.defaultFactory(), diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt index 4cb13671c5d..b63a26d942c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/MessagePreviewFormatter.kt @@ -30,8 +30,10 @@ import io.getstream.chat.android.client.utils.message.isPollClosed import io.getstream.chat.android.client.utils.message.isSystem import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.ui.attachments.AttachmentFactory +import io.getstream.chat.android.compose.ui.theme.StreamColors import io.getstream.chat.android.compose.ui.theme.StreamTypography import io.getstream.chat.android.models.Attachment +import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User @@ -57,6 +59,15 @@ public interface MessagePreviewFormatter { */ public fun formatMessagePreview(message: Message, currentUser: User?): AnnotatedString + /** + * Generates a preview text for the given draft message. + * This is used to show a preview of the draft message in the the channel list. + * + * @param draftMessage The draft message whose data is used to generate the preview text. + * @return The formatted text representation for the given draft message. + */ + public fun formatDraftMessagePreview(draftMessage: DraftMessage): AnnotatedString + public companion object { /** * Builds the default message preview text formatter. @@ -74,10 +85,12 @@ public interface MessagePreviewFormatter { autoTranslationEnabled: Boolean, typography: StreamTypography, attachmentFactories: List, + colors: StreamColors, ): MessagePreviewFormatter { return DefaultMessagePreviewFormatter( context = context, autoTranslationEnabled = autoTranslationEnabled, + draftMessageLabelTextStyle = typography.footnoteBold.copy(color = colors.primaryAccent), messageTextStyle = typography.bodyBold, senderNameTextStyle = typography.bodyBold, attachmentTextFontStyle = typography.bodyItalic, @@ -93,9 +106,11 @@ public interface MessagePreviewFormatter { * * @param context The context to load string resources. */ +@Suppress("LongParameterList") private class DefaultMessagePreviewFormatter( private val context: Context, private val autoTranslationEnabled: Boolean, + private val draftMessageLabelTextStyle: TextStyle, private val messageTextStyle: TextStyle, private val senderNameTextStyle: TextStyle, private val attachmentTextFontStyle: TextStyle, @@ -191,6 +206,35 @@ private class DefaultMessagePreviewFormatter( } } + /** + * Generates a preview text for the given draft message. + * This is used to show a preview of the draft message in the the channel list. + * + * @param draftMessage The draft message whose data is used to generate the preview text. + * @return The formatted text representation for the given draft message. + */ + override fun formatDraftMessagePreview(draftMessage: DraftMessage): AnnotatedString = buildAnnotatedString { + withStyle( + style = SpanStyle( + fontStyle = draftMessageLabelTextStyle.fontStyle, + fontFamily = draftMessageLabelTextStyle.fontFamily, + color = draftMessageLabelTextStyle.color, + ), + ) { + append(context.getString(R.string.stream_compose_channel_list_draft)) + } + append(SPACE) + withStyle( + style = SpanStyle( + fontStyle = messageTextStyle.fontStyle, + fontFamily = messageTextStyle.fontFamily, + color = messageTextStyle.color, + ), + ) { + append(draftMessage.text) + } + } + /** * Appends the sender name to the [AnnotatedString]. */ diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt index 0ad82be8ce0..8cc5d49c168 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModel.kt @@ -33,6 +33,7 @@ import io.getstream.chat.android.core.utils.Debouncer import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.ChannelMute import io.getstream.chat.android.models.ConnectionState +import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.FilterObject import io.getstream.chat.android.models.Filters import io.getstream.chat.android.models.Message @@ -77,6 +78,7 @@ import kotlin.coroutines.cancellation.CancellationException * @param messageLimit How many messages are fetched for each channel item when loading channels. * @param chatEventHandlerFactory The instance of [ChatEventHandlerFactory] used to create [ChatEventHandler]. * @param searchDebounceMs The debounce time for search queries. + * @param isDraftMessageEnabled If the draft message feature is enabled. */ @Suppress("TooManyFunctions") public class ChannelListViewModel( @@ -88,6 +90,7 @@ public class ChannelListViewModel( private val messageLimit: Int = DEFAULT_MESSAGE_LIMIT, private val chatEventHandlerFactory: ChatEventHandlerFactory = ChatEventHandlerFactory(chatClient.clientState), searchDebounceMs: Long = SEARCH_DEBOUNCE_MS, + private val isDraftMessageEnabled: Boolean, ) : ViewModel() { private val logger by taggedLogger("Chat:ChannelListVM") @@ -392,7 +395,8 @@ public class ChannelListViewModel( queryChannelsState.channelsStateData, channelMutes, chatClient.globalState.typingChannels, - ) { state, channelMutes, typingChannels -> + chatClient.globalState.channelDraftMessages, + ) { state, channelMutes, typingChannels, channelDraftMessages -> when (state) { ChannelsStateData.NoQueryActive, ChannelsStateData.Loading, @@ -418,6 +422,7 @@ public class ChannelListViewModel( channels = state.channels, channelMutes = channelMutes, typingEvents = typingChannels, + draftMessages = channelDraftMessages.takeIf { isDraftMessageEnabled } ?: emptyMap(), ), isLoadingMore = false, endOfChannels = queryChannelsState.endOfChannels.value, @@ -705,6 +710,7 @@ public class ChannelListViewModel( channels: List, channelMutes: List, typingEvents: Map, + draftMessages: Map, ): List { val mutedChannelIds = channelMutes.map { channelMute -> channelMute.channel?.cid }.toSet() return channels.map { @@ -712,6 +718,7 @@ public class ChannelListViewModel( channel = it, isMuted = it.cid in mutedChannelIds, typingUsers = typingEvents[it.cid]?.users ?: emptyList(), + draftMessage = draftMessages[it.cid], ) } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelViewModelFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelViewModelFactory.kt index aea99c6149c..a00dfad7c4b 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelViewModelFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/channels/ChannelViewModelFactory.kt @@ -46,6 +46,7 @@ public class ChannelViewModelFactory( private val memberLimit: Int = ChannelListViewModel.DEFAULT_MEMBER_LIMIT, private val messageLimit: Int = ChannelListViewModel.DEFAULT_MESSAGE_LIMIT, private val chatEventHandlerFactory: ChatEventHandlerFactory = ChatEventHandlerFactory(chatClient.clientState), + private val isDraftMessageEnabled: Boolean = false, ) : ViewModelProvider.Factory { private val factories: Map, () -> ViewModel> = mapOf( @@ -58,6 +59,7 @@ public class ChannelViewModelFactory( messageLimit = messageLimit, memberLimit = memberLimit, chatEventHandlerFactory = chatEventHandlerFactory, + isDraftMessageEnabled = isDraftMessageEnabled, ) }, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt index d96713d76c3..ffc56afae87 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory.kt @@ -73,6 +73,8 @@ import java.io.File * @param showThreadSeparatorInEmptyThread Configures if we show a thread separator when threads are empty. * Adds the separator item when the value is `true`. * @param threadLoadOlderToNewer Configures if the thread should load older messages to newer messages. + * @param isComposerLinkPreviewEnabled If the link preview is enabled in the composer. + * @param isComposerDraftMessageEnabled If the draft message is enabled in the composer. */ public class MessagesViewModelFactory( private val context: Context, @@ -104,6 +106,7 @@ public class MessagesViewModelFactory( private val showThreadSeparatorInEmptyThread: Boolean = false, private val threadLoadOlderToNewer: Boolean = false, private val isComposerLinkPreviewEnabled: Boolean = false, + private val isComposerDraftMessageEnabled: Boolean = false, ) : ViewModelProvider.Factory { private val channelStateFlow: StateFlow by lazy { @@ -127,8 +130,11 @@ public class MessagesViewModelFactory( userLookupHandler = userLookupHandler, fileToUri = fileToUriConverter, channelCid = channelId, - maxAttachmentCount = maxAttachmentCount, - isLinkPreviewEnabled = isComposerLinkPreviewEnabled, + config = MessageComposerController.Config( + maxAttachmentCount = maxAttachmentCount, + isLinkPreviewEnabled = isComposerLinkPreviewEnabled, + isDraftMessageEnabled = isComposerDraftMessageEnabled, + ), ), ) }, 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 64b847a90a9..2e26ed024dd 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -23,6 +23,7 @@ Search channels by name + Draft: You No channels available No results for \"%1$s\" diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt index 40db32fd787..09d2ba5b4d7 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelTest.kt @@ -357,6 +357,7 @@ internal class ChannelListViewModelTest { whenever(chatClient.channel(any())) doReturn channelClient whenever(chatClient.channel(any(), any())) doReturn channelClient whenever(chatClient.clientState) doReturn clientState + whenever(globalState.channelDraftMessages) doReturn MutableStateFlow(emptyMap()) } fun givenCurrentUser(currentUser: User = User(id = "Jc")) = apply { @@ -417,6 +418,7 @@ internal class ChannelListViewModelTest { chatClient = chatClient, initialSort = initialSort, initialFilters = initialFilters, + isDraftMessageEnabled = false, chatEventHandlerFactory = ChatEventHandlerFactory(clientState), ) testScope.advanceUntilIdle() diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModelTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModelTest.kt index fb21627bfce..44b392be206 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModelTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModelTest.kt @@ -439,7 +439,10 @@ internal class MessageComposerViewModelTest { mediaRecorder = mock(), userLookupHandler = DefaultUserLookupHandler(chatClient, channelId), fileToUri = { it.path }, - maxAttachmentCount = maxAttachmentCount, + globalState = globalState, + config = MessageComposerController.Config( + maxAttachmentCount = maxAttachmentCount, + ), channelState = MutableStateFlow(channelState), ), ) diff --git a/stream-chat-android-docs/src/main/java/io/getstream/chat/docs/java/client/docusaurus/ChannelListUpdates.java b/stream-chat-android-docs/src/main/java/io/getstream/chat/docs/java/client/docusaurus/ChannelListUpdates.java index c05a9dfd845..73dd0eff6f9 100644 --- a/stream-chat-android-docs/src/main/java/io/getstream/chat/docs/java/client/docusaurus/ChannelListUpdates.java +++ b/stream-chat-android-docs/src/main/java/io/getstream/chat/docs/java/client/docusaurus/ChannelListUpdates.java @@ -109,12 +109,14 @@ public void applyToViewModel(ChatEventHandlerFactory chatEventHandlerFactory) { int limit = 30; int messageLimit = 1; int memberLimit = 30; + boolean isDraftMessagesEnabled = false; ChannelListViewModelFactory factory = new ChannelListViewModelFactory( filter, sort, limit, messageLimit, memberLimit, + isDraftMessagesEnabled, chatEventHandlerFactory ); } diff --git a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/general/ChatTheme.kt b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/general/ChatTheme.kt index 7a4a8941115..e959ebadec1 100644 --- a/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/general/ChatTheme.kt +++ b/stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/compose/general/ChatTheme.kt @@ -26,6 +26,7 @@ import io.getstream.chat.android.compose.ui.util.MessagePreviewFormatter import io.getstream.chat.android.compose.ui.util.MessageTextFormatter import io.getstream.chat.android.compose.ui.util.QuotedMessageTextFormatter import io.getstream.chat.android.compose.viewmodel.messages.MessagesViewModelFactory +import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User import io.getstream.chat.android.ui.common.helper.DateFormatter @@ -219,6 +220,13 @@ private object ChatThemeMessageTextFormatterDefaultSnippet : ChatThemeCustomizat // add your custom styling here } } + + override fun formatDraftMessagePreview(draftMessage: DraftMessage): AnnotatedString { + return buildAnnotatedString { + append(draftMessage.text) + // add your custom styling here + } + } } return MessageTextFormatter.defaultFormatter( autoTranslationEnabled = autoTranslationEnabled, diff --git a/stream-chat-android-previewdata/src/main/kotlin/io/getstream/chat/android/previewdata/PreviewMessageData.kt b/stream-chat-android-previewdata/src/main/kotlin/io/getstream/chat/android/previewdata/PreviewMessageData.kt index 4264aaf70e3..07b38fe27fe 100644 --- a/stream-chat-android-previewdata/src/main/kotlin/io/getstream/chat/android/previewdata/PreviewMessageData.kt +++ b/stream-chat-android-previewdata/src/main/kotlin/io/getstream/chat/android/previewdata/PreviewMessageData.kt @@ -16,6 +16,7 @@ package io.getstream.chat.android.previewdata +import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.MessageType import io.getstream.chat.android.models.Reaction @@ -106,4 +107,10 @@ public object PreviewMessageData { type = MessageType.REGULAR, mentionedUsers = listOf(PreviewUserData.user7), ) + + public val draftMessage: DraftMessage = DraftMessage( + id = "draft-message", + cid = "channel-id", + text = "Some text for the draft message", + ) } diff --git a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api index a48d15b5325..0e728273c0e 100644 --- a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api +++ b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api @@ -951,6 +951,14 @@ public final class io/getstream/chat/android/ui/common/state/messages/MessageInp public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/common/state/messages/MessageInput$Source$DraftMessage : io/getstream/chat/android/ui/common/state/messages/MessageInput$Source$Internal { + public static final field $stable I + public static final field INSTANCE Lio/getstream/chat/android/ui/common/state/messages/MessageInput$Source$DraftMessage; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/ui/common/state/messages/MessageInput$Source$Edit : io/getstream/chat/android/ui/common/state/messages/MessageInput$Source$Internal { public static final field $stable I public static final field INSTANCE Lio/getstream/chat/android/ui/common/state/messages/MessageInput$Source$Edit; diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt index 77ce3a43750..d4665ca35cb 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt @@ -25,10 +25,13 @@ import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider import io.getstream.chat.android.models.Attachment import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.models.Command +import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.LinkPreview import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.PollConfig import io.getstream.chat.android.models.User +import io.getstream.chat.android.state.extensions.globalState +import io.getstream.chat.android.state.plugin.state.global.GlobalState import io.getstream.chat.android.ui.common.feature.messages.composer.mention.UserLookupHandler import io.getstream.chat.android.ui.common.feature.messages.composer.typing.TypingSuggester import io.getstream.chat.android.ui.common.feature.messages.composer.typing.TypingSuggestionOptions @@ -66,6 +69,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest @@ -94,8 +98,7 @@ import java.util.regex.Pattern * @param mediaRecorder The media recorder used to record audio messages. * @param userLookupHandler The handler used to lookup users for mentions. * @param fileToUri The function used to convert a file to a URI. - * @param maxAttachmentCount The maximum number of attachments that can be sent in a single message. - * @param isLinkPreviewEnabled If the link preview is enabled in the channel. + * @param config The configuration for the message composer. * */ @OptIn(ExperimentalCoroutinesApi::class) @@ -104,19 +107,23 @@ import java.util.regex.Pattern public class MessageComposerController( private val channelCid: String, private val chatClient: ChatClient = ChatClient.instance(), + private val globalState: GlobalState = ChatClient.instance().globalState, public val channelState: StateFlow, mediaRecorder: StreamMediaRecorder, private val userLookupHandler: UserLookupHandler, fileToUri: (File) -> String, - maxAttachmentCount: Int = AttachmentConstants.MAX_ATTACHMENTS_COUNT, - private val isLinkPreviewEnabled: Boolean = false, + private val config: MessageComposerController.Config = MessageComposerController.Config(), ) { + private val channelType = channelCid.cidToTypeAndId().first + private val channelId = channelCid.cidToTypeAndId().second private val messageValidator = MessageValidator( appSettings = chatClient.getAppSettings(), - maxAttachmentCount = maxAttachmentCount, + maxAttachmentCount = config.maxAttachmentCount, ) + private var currentDraftId: String? = null + /** * The logger used to print to errors, warnings, information * and other things to log. @@ -386,7 +393,9 @@ public class MessageComposerController( * Sets up the observing operations for various composer states. */ @OptIn(FlowPreview::class) + @Suppress("LongMethod") private fun setupComposerState() { + fetchDraftMessage(messageMode.value) messageInput.onEach { value -> input.value = value.text state.value = state.value.copy(inputValue = value.text) @@ -429,9 +438,19 @@ public class MessageComposerController( state.value = state.value.copy(coolDownTime = cooldownTimer) }.launchIn(scope) - messageMode.onEach { messageMode -> - state.value = state.value.copy(messageMode = messageMode) - }.launchIn(scope) + messageMode + .distinctUntilChanged { old, new -> + when (old) { + is MessageMode.Normal -> new is MessageMode.Normal + is MessageMode.MessageThread -> + old.parentMessage.id == (new as? MessageMode.MessageThread)?.parentMessage?.id + } + } + .onEach { messageMode -> + saveDraftMessage(state.value.messageMode) + state.value = state.value.copy(messageMode = messageMode) + fetchDraftMessage(messageMode) + }.launchIn(scope) alsoSendToChannel.onEach { alsoSendToChannel -> state.value = state.value.copy(alsoSendToChannel = alsoSendToChannel) @@ -452,6 +471,26 @@ public class MessageComposerController( selectedAttachments.value = selectedAttachments.value + recording.attachment } }.launchIn(scope) + + if (config.isDraftMessageEnabled) { + globalState.channelDraftMessages.onEach { + if (it[channelCid] == null && + currentDraftId != null && + messageMode.value is MessageMode.Normal + ) { + clearData() + } + }.launchIn(scope) + + globalState.threadDraftMessages.onEach { + if (it[parentMessageId] == null && + currentDraftId != null && + messageMode.value is MessageMode.MessageThread + ) { + clearData() + } + }.launchIn(scope) + } } /** @@ -469,6 +508,41 @@ public class MessageComposerController( this.messageInput.value = MessageInput(value, source) } + private suspend fun saveDraftMessage(messageMode: MessageMode) { + if (!config.isDraftMessageEnabled) return + currentDraftId = null + when (val messageText = messageInput.value.text) { + "" -> clearDraftMessage(messageMode) + else -> { + globalState.getDraftMessageOrEmpty(messageMode).let { + chatClient.createDraftMessage( + channelType = channelType, + channelId = channelId, + message = it.copy( + text = messageText, + showInChannel = alsoSendToChannel.value, + replyMessage = (messageActions.value.firstOrNull { it is Reply } as? Reply)?.message, + ), + ).await() + } + } + } + } + + private fun fetchDraftMessage(messageMode: MessageMode) { + if (!config.isDraftMessageEnabled) return + globalState.getDraftMessageOrEmpty(messageMode).let { draftMessage -> + currentDraftId = draftMessage.id + setMessageInputInternal(draftMessage.text, MessageInput.Source.DraftMessage) + setAlsoSendToChannel(draftMessage.showInChannel) + draftMessage.replyMessage + ?.let { performMessageAction(Reply(it)) } + ?: run { + messageActions.value = messageActions.value.filterNot { it is Reply }.toSet() + } + } + } + /** * Called when the message mode changes and the internal state needs to be updated. * @@ -569,14 +643,12 @@ public class MessageComposerController( * @param pollConfig Configuration for creating a poll. */ public fun createPoll(pollConfig: PollConfig, onResult: (Result) -> Unit = {}) { - channelCid.cidToTypeAndId().let { (channelType, channelId) -> - chatClient.sendPoll( - channelType = channelType, - channelId = channelId, - pollConfig = pollConfig, - ).enqueue { response -> - onResult(response) - } + chatClient.sendPoll( + channelType = channelType, + channelId = channelId, + pollConfig = pollConfig, + ).enqueue { response -> + onResult(response) } } @@ -586,12 +658,25 @@ public class MessageComposerController( */ public fun clearData() { logger.i { "[clearData]" } + dismissMessageActions() + scope.launch { clearDraftMessage(messageMode.value) } messageInput.value = MessageInput() selectedAttachments.value = emptyList() validationErrors.value = emptyList() alsoSendToChannel.value = false } + private suspend fun clearDraftMessage(messageMode: MessageMode) { + if (!config.isDraftMessageEnabled) return + globalState.getDraftMessage(messageMode)?.let { draftMessage -> + chatClient.deleteDraftMessages( + channelType = channelType, + channelId = channelId, + message = draftMessage, + ).await() + } + } + /** * Sends a given message using our Stream API. Based on [isInEditMode], we either edit an existing message, or we * send a new message, using [ChatClient]. In case the message is a moderated message the old one is deleted before @@ -628,7 +713,6 @@ public class MessageComposerController( } } } - dismissMessageActions() clearData() sendMessageCall.enqueue(callback) } @@ -712,7 +796,10 @@ public class MessageComposerController( public fun onCleared() { typingUpdatesBuffer.clear() audioRecordingController.onCleared() - scope.cancel() + scope.launch { + saveDraftMessage(messageMode.value) + scope.cancel() + } } /** @@ -892,7 +979,7 @@ public class MessageComposerController( * Shows link previews if necessary. */ private suspend fun handleLinkPreviews() { - if (!isLinkPreviewEnabled) return + if (!config.isLinkPreviewEnabled) return val urls = LinkPattern.findAll(messageText).map { it.value }.toList() @@ -926,18 +1013,14 @@ public class MessageComposerController( * Makes an API call signaling that a typing event has occurred. */ private fun sendKeystrokeEvent() { - val (type, id) = channelCid.cidToTypeAndId() - - chatClient.keystroke(type, id, parentMessageId).enqueue() + chatClient.keystroke(channelType, channelId, parentMessageId).enqueue() } /** * Makes an API call signaling that a stop typing event has occurred. */ private fun sendStopTypingEvent() { - val (type, id) = channelCid.cidToTypeAndId() - - chatClient.stopTyping(type, id, parentMessageId).enqueue() + chatClient.stopTyping(channelType, channelId, parentMessageId).enqueue() } private fun ChatClient.enrichPreview(url: String): Call { @@ -968,4 +1051,24 @@ public class MessageComposerController( */ private const val TEXT_INPUT_DEBOUNCE_TIME = 300L } + + @InternalStreamChatApi + public data class Config( + val maxAttachmentCount: Int = AttachmentConstants.MAX_ATTACHMENTS_COUNT, + val isLinkPreviewEnabled: Boolean = false, + val isDraftMessageEnabled: Boolean = false, + ) + + private fun GlobalState.getDraftMessageOrEmpty(messageMode: MessageMode): DraftMessage = + getDraftMessage(messageMode) ?: messageMode.emptyDraftMessage() + + private fun GlobalState.getDraftMessage(messageMode: MessageMode): DraftMessage? = when (messageMode) { + is MessageMode.MessageThread -> threadDraftMessages.value[messageMode.parentMessage.id] + else -> channelDraftMessages.value[channelCid] + } + + private fun MessageMode.emptyDraftMessage(): DraftMessage = when (this) { + is MessageMode.MessageThread -> DraftMessage(cid = channelCid, parentId = parentMessage.id) + else -> DraftMessage(cid = channelCid) + } } diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/MessageInput.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/MessageInput.kt index 7373a7a0de2..96a4e326cc7 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/MessageInput.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/MessageInput.kt @@ -51,5 +51,7 @@ public data class MessageInput( * The message was created internally by the SDK */ public data object MentionSelected : Internal() + + public data object DraftMessage : Internal() } } diff --git a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTests.kt b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTests.kt index f9c035b6a92..abacee16def 100644 --- a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTests.kt +++ b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTests.kt @@ -26,6 +26,7 @@ import io.getstream.chat.android.models.Command import io.getstream.chat.android.models.Config import io.getstream.chat.android.models.Member import io.getstream.chat.android.models.User +import io.getstream.chat.android.state.plugin.state.global.GlobalState import io.getstream.chat.android.test.TestCoroutineExtension import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -117,6 +118,7 @@ internal class MessageComposerControllerTests { private val clientState: ClientState = mock() private val channelState: ChannelState = mock() + private val globalState: GlobalState = mock() fun givenAppSettings(appSettings: AppSettings) = apply { whenever(chatClient.getAppSettings()) doReturn appSettings @@ -154,6 +156,7 @@ internal class MessageComposerControllerTests { mediaRecorder = mock(), userLookupHandler = mock(), fileToUri = mock(), + globalState = globalState, ) } } diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/application/ChatInitializer.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/application/ChatInitializer.kt index eb42ca8764a..d8c74cc30b7 100644 --- a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/application/ChatInitializer.kt +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/application/ChatInitializer.kt @@ -113,6 +113,7 @@ class ChatInitializer( val messageTranslator = MessageTranslator(client::getCurrentUser, autoTranslationEnabled) ChatUI.autoTranslationEnabled = autoTranslationEnabled ChatUI.messageTextTransformer = MarkdownTextTransformer(context, messageTranslator) + ChatUI.draftMessagesEnabled = true TransformStyle.viewReactionsStyleTransformer = StyleTransformer { defaultStyle -> defaultStyle.copy( diff --git a/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api b/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api index b1bc0a02c61..dc78871631f 100644 --- a/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api +++ b/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api @@ -10,6 +10,7 @@ public final class io/getstream/chat/android/ui/ChatUI { public static final fun getDecoratorProviderFactory ()Lio/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/decorator/DecoratorProviderFactory; public static final fun getDownloadAttachmentUriGenerator ()Lio/getstream/chat/android/ui/common/helper/DownloadAttachmentUriGenerator; public static final fun getDownloadRequestInterceptor ()Lio/getstream/chat/android/ui/common/helper/DownloadRequestInterceptor; + public static final fun getDraftMessagesEnabled ()Z public static final fun getFonts ()Lio/getstream/chat/android/ui/font/ChatFonts; public static final fun getImageAssetTransformer ()Lio/getstream/chat/android/ui/common/helper/ImageAssetTransformer; public static final fun getImageHeadersProvider ()Lio/getstream/chat/android/ui/common/helper/ImageHeadersProvider; @@ -34,6 +35,7 @@ public final class io/getstream/chat/android/ui/ChatUI { public static final fun setDecoratorProviderFactory (Lio/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/decorator/DecoratorProviderFactory;)V public static final fun setDownloadAttachmentUriGenerator (Lio/getstream/chat/android/ui/common/helper/DownloadAttachmentUriGenerator;)V public static final fun setDownloadRequestInterceptor (Lio/getstream/chat/android/ui/common/helper/DownloadRequestInterceptor;)V + public static final fun setDraftMessagesEnabled (Z)V public static final fun setFonts (Lio/getstream/chat/android/ui/font/ChatFonts;)V public static final fun setImageAssetTransformer (Lio/getstream/chat/android/ui/common/helper/ImageAssetTransformer;)V public static final fun setImageHeadersProvider (Lio/getstream/chat/android/ui/common/helper/ImageHeadersProvider;)V @@ -335,45 +337,47 @@ public final class io/getstream/chat/android/ui/feature/channels/list/ChannelLis } public final class io/getstream/chat/android/ui/feature/channels/list/ChannelListViewStyle : io/getstream/chat/android/ui/helper/ViewStyle { - public fun (Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ZZZIILio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ILio/getstream/chat/android/ui/font/TextStyle;ILandroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;IIILjava/lang/Integer;ZZIIIIIF)V + public fun (Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ZZZIILio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ILio/getstream/chat/android/ui/font/TextStyle;ILandroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;IIILjava/lang/Integer;ZZIIIIIF)V public final fun component1 ()Landroid/graphics/drawable/Drawable; public final fun component10 ()Lio/getstream/chat/android/ui/font/TextStyle; - public final fun component11 ()Landroid/graphics/drawable/Drawable; + public final fun component11 ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun component12 ()Landroid/graphics/drawable/Drawable; public final fun component13 ()Landroid/graphics/drawable/Drawable; - public final fun component14 ()I - public final fun component15 ()Lio/getstream/chat/android/ui/font/TextStyle; - public final fun component16 ()I - public final fun component17 ()Landroid/graphics/drawable/Drawable; + public final fun component14 ()Landroid/graphics/drawable/Drawable; + public final fun component15 ()I + public final fun component16 ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun component17 ()I public final fun component18 ()Landroid/graphics/drawable/Drawable; - public final fun component19 ()I + public final fun component19 ()Landroid/graphics/drawable/Drawable; public final fun component2 ()Landroid/graphics/drawable/Drawable; public final fun component20 ()I public final fun component21 ()I - public final fun component22 ()Ljava/lang/Integer; - public final fun component23 ()Z + public final fun component22 ()I + public final fun component23 ()Ljava/lang/Integer; public final fun component24 ()Z - public final fun component25 ()I + public final fun component25 ()Z public final fun component26 ()I public final fun component27 ()I public final fun component28 ()I public final fun component29 ()I public final fun component3 ()Z - public final fun component30 ()F + public final fun component30 ()I + public final fun component31 ()F public final fun component4 ()Z public final fun component5 ()Z public final fun component6 ()I public final fun component7 ()I public final fun component8 ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun component9 ()Lio/getstream/chat/android/ui/font/TextStyle; - public final fun copy (Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ZZZIILio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ILio/getstream/chat/android/ui/font/TextStyle;ILandroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;IIILjava/lang/Integer;ZZIIIIIF)Lio/getstream/chat/android/ui/feature/channels/list/ChannelListViewStyle; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/channels/list/ChannelListViewStyle;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ZZZIILio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ILio/getstream/chat/android/ui/font/TextStyle;ILandroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;IIILjava/lang/Integer;ZZIIIIIFILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/channels/list/ChannelListViewStyle; + public final fun copy (Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ZZZIILio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ILio/getstream/chat/android/ui/font/TextStyle;ILandroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;IIILjava/lang/Integer;ZZIIIIIF)Lio/getstream/chat/android/ui/feature/channels/list/ChannelListViewStyle; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/channels/list/ChannelListViewStyle;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ZZZIILio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;ILio/getstream/chat/android/ui/font/TextStyle;ILandroid/graphics/drawable/Drawable;Landroid/graphics/drawable/Drawable;IIILjava/lang/Integer;ZZIIIIIFILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/channels/list/ChannelListViewStyle; public fun equals (Ljava/lang/Object;)Z public final fun getBackgroundColor ()I public final fun getBackgroundLayoutColor ()I public final fun getChannelTitleText ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun getDeleteEnabled ()Z public final fun getDeleteIcon ()Landroid/graphics/drawable/Drawable; + public final fun getDraftMessageLabel ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun getEdgeEffectColor ()Ljava/lang/Integer; public final fun getEmptyStateView ()I public final fun getForegroundLayoutColor ()I @@ -407,13 +411,15 @@ public abstract class io/getstream/chat/android/ui/feature/channels/list/adapter } public final class io/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem$ChannelItem : io/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem { - public fun (Lio/getstream/chat/android/models/Channel;Ljava/util/List;)V + public fun (Lio/getstream/chat/android/models/Channel;Ljava/util/List;Lio/getstream/chat/android/models/DraftMessage;)V public final fun component1 ()Lio/getstream/chat/android/models/Channel; public final fun component2 ()Ljava/util/List; - public final fun copy (Lio/getstream/chat/android/models/Channel;Ljava/util/List;)Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem$ChannelItem; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem$ChannelItem;Lio/getstream/chat/android/models/Channel;Ljava/util/List;ILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem$ChannelItem; + public final fun component3 ()Lio/getstream/chat/android/models/DraftMessage; + public final fun copy (Lio/getstream/chat/android/models/Channel;Ljava/util/List;Lio/getstream/chat/android/models/DraftMessage;)Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem$ChannelItem; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem$ChannelItem;Lio/getstream/chat/android/models/Channel;Ljava/util/List;Lio/getstream/chat/android/models/DraftMessage;ILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem$ChannelItem; public fun equals (Ljava/lang/Object;)Z public final fun getChannel ()Lio/getstream/chat/android/models/Channel; + public final fun getDraftMessage ()Lio/getstream/chat/android/models/DraftMessage; public final fun getTypingUsers ()Ljava/util/List; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -431,7 +437,7 @@ public final class io/getstream/chat/android/ui/feature/channels/list/adapter/Ch } public final class io/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListPayloadDiff { - public fun (ZZZZZZZZ)V + public fun (ZZZZZZZZZ)V public final fun component1 ()Z public final fun component2 ()Z public final fun component3 ()Z @@ -440,10 +446,12 @@ public final class io/getstream/chat/android/ui/feature/channels/list/adapter/Ch public final fun component6 ()Z public final fun component7 ()Z public final fun component8 ()Z - public final fun copy (ZZZZZZZZ)Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListPayloadDiff; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListPayloadDiff;ZZZZZZZZILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListPayloadDiff; + public final fun component9 ()Z + public final fun copy (ZZZZZZZZZ)Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListPayloadDiff; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListPayloadDiff;ZZZZZZZZZILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListPayloadDiff; public fun equals (Ljava/lang/Object;)Z public final fun getAvatarViewChanged ()Z + public final fun getDraftMessageChanged ()Z public final fun getExtraDataChanged ()Z public final fun getLastMessageChanged ()Z public final fun getNameChanged ()Z @@ -4275,10 +4283,10 @@ public final class io/getstream/chat/android/ui/viewmodel/channels/ChannelListHe public final class io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModel : androidx/lifecycle/ViewModel { public static final field Companion Lio/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModel$Companion; public static final field DEFAULT_SORT Lio/getstream/chat/android/models/querysort/QuerySorter; - public fun ()V - public fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/state/plugin/state/global/GlobalState;)V - public synthetic fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/state/plugin/state/global/GlobalState;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;IIIZLio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/state/plugin/state/global/GlobalState;)V + public synthetic fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;IIIZLio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/state/plugin/state/global/GlobalState;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun deleteChannel (Lio/getstream/chat/android/models/Channel;)V + public final fun getDraftMessages ()Landroidx/lifecycle/LiveData; public final fun getErrorEvents ()Landroidx/lifecycle/LiveData; public final fun getPaginationState ()Landroidx/lifecycle/LiveData; public final fun getState ()Landroidx/lifecycle/LiveData; @@ -4378,8 +4386,9 @@ public final class io/getstream/chat/android/ui/viewmodel/channels/ChannelListVi public fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;I)V public fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;II)V public fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;III)V - public fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;)V - public synthetic fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;IIILio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;IIIZ)V + public fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;IIIZLio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;)V + public synthetic fun (Lio/getstream/chat/android/models/FilterObject;Lio/getstream/chat/android/models/querysort/QuerySorter;IIIZLio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun create (Ljava/lang/Class;)Landroidx/lifecycle/ViewModel; } @@ -4388,6 +4397,7 @@ public final class io/getstream/chat/android/ui/viewmodel/channels/ChannelListVi public final fun build ()Landroidx/lifecycle/ViewModelProvider$Factory; public final fun chatEventHandlerFactory (Lio/getstream/chat/android/state/event/handler/chat/factory/ChatEventHandlerFactory;)Lio/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelFactory$Builder; public final fun filter (Lio/getstream/chat/android/models/FilterObject;)Lio/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelFactory$Builder; + public final fun isDraftMessagesEnabled (Z)Lio/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelFactory$Builder; public final fun limit (I)Lio/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelFactory$Builder; public final fun memberLimit (I)Lio/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelFactory$Builder; public final fun messageLimit (I)Lio/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelFactory$Builder; @@ -5028,7 +5038,8 @@ public final class io/getstream/chat/android/ui/viewmodel/messages/MessageListVi public fun (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/UserLookupHandler;Lkotlin/jvm/functions/Function1;IZIZLio/getstream/chat/android/ui/common/state/messages/list/DeletedMessageVisibility;Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/MessagePositionHandler;Z)V public fun (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/UserLookupHandler;Lkotlin/jvm/functions/Function1;IZIZLio/getstream/chat/android/ui/common/state/messages/list/DeletedMessageVisibility;Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/MessagePositionHandler;ZZ)V public fun (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/UserLookupHandler;Lkotlin/jvm/functions/Function1;IZIZLio/getstream/chat/android/ui/common/state/messages/list/DeletedMessageVisibility;Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/MessagePositionHandler;ZZZ)V - public synthetic fun (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/UserLookupHandler;Lkotlin/jvm/functions/Function1;IZIZLio/getstream/chat/android/ui/common/state/messages/list/DeletedMessageVisibility;Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/MessagePositionHandler;ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/UserLookupHandler;Lkotlin/jvm/functions/Function1;IZIZLio/getstream/chat/android/ui/common/state/messages/list/DeletedMessageVisibility;Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/MessagePositionHandler;ZZZZ)V + public synthetic fun (Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lio/getstream/sdk/chat/audio/recording/StreamMediaRecorder;Lio/getstream/chat/android/ui/common/feature/messages/composer/mention/UserLookupHandler;Lkotlin/jvm/functions/Function1;IZIZLio/getstream/chat/android/ui/common/state/messages/list/DeletedMessageVisibility;Lio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler;Lio/getstream/chat/android/ui/common/feature/messages/list/MessagePositionHandler;ZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun create (Ljava/lang/Class;)Landroidx/lifecycle/ViewModel; } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/ChatUI.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/ChatUI.kt index 06e1f72054d..68e91bb9410 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/ChatUI.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/ChatUI.kt @@ -195,6 +195,12 @@ public object ChatUI { @JvmStatic public var videoThumbnailsEnabled: Boolean = true + /** + * Whether draft messages are enabled. + */ + @JvmStatic + public var draftMessagesEnabled: Boolean = false + /** * Sets the strategy for resizing images hosted on Stream's CDN. Disabled by default, * set [StreamCdnImageResizing.imageResizingEnabled] to true if you wish to enable resizing images. Note that diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/ChannelListViewStyle.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/ChannelListViewStyle.kt index f9d0042bf2f..5bf394b6366 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/ChannelListViewStyle.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/ChannelListViewStyle.kt @@ -79,6 +79,7 @@ public data class ChannelListViewStyle( @ColorInt public val backgroundColor: Int, @ColorInt public val backgroundLayoutColor: Int, public val channelTitleText: TextStyle, + public val draftMessageLabel: TextStyle, public val lastMessageText: TextStyle, public val lastMessageDateText: TextStyle, public val indicatorSentIcon: Drawable, @@ -185,6 +186,25 @@ public data class ChannelListViewStyle( ) .build() + val draftMessageLabel = TextStyle.Builder(a) + .size( + R.styleable.ChannelListView_streamUiLastMessageTextSize, + context.getDimension(R.dimen.stream_ui_channel_item_message), + ) + .color( + R.styleable.ChannelListView_streamUiDraftMessageLabelTextColor, + context.getColorCompat(R.color.stream_ui_accent_blue), + ) + .font( + R.styleable.ChannelListView_streamUiLastMessageFontAssets, + R.styleable.ChannelListView_streamUiLastMessageTextFont, + ) + .style( + R.styleable.ChannelListView_streamUiLastMessageTextStyle, + Typeface.NORMAL, + ) + .build() + val lastMessageDateText = TextStyle.Builder(a) .size( R.styleable.ChannelListView_streamUiLastMessageDateTextSize, @@ -315,6 +335,7 @@ public data class ChannelListViewStyle( backgroundColor = backgroundColor, backgroundLayoutColor = backgroundLayoutColor, channelTitleText = channelTitleText, + draftMessageLabel = draftMessageLabel, lastMessageText = lastMessageText, lastMessageDateText = lastMessageDateText, indicatorSentIcon = indicatorSentIcon, diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem.kt index e0a1bc5ba6f..df36dd119b0 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListItem.kt @@ -17,10 +17,15 @@ package io.getstream.chat.android.ui.feature.channels.list.adapter import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.User public sealed class ChannelListItem { - public data class ChannelItem(val channel: Channel, val typingUsers: List) : ChannelListItem() + public data class ChannelItem( + val channel: Channel, + val typingUsers: List, + val draftMessage: DraftMessage?, + ) : ChannelListItem() public object LoadingMoreItem : ChannelListItem() { override fun toString(): String = "LoadingMoreItem" } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListPayloadDiff.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListPayloadDiff.kt index 01864e4b7f0..4c1beb863d7 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListPayloadDiff.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/ChannelListPayloadDiff.kt @@ -25,6 +25,7 @@ public data class ChannelListPayloadDiff( val unreadCountChanged: Boolean, val extraDataChanged: Boolean, val typingUsersChanged: Boolean, + val draftMessageChanged: Boolean, ) { public fun hasDifference(): Boolean = nameChanged @@ -35,6 +36,7 @@ public data class ChannelListPayloadDiff( .or(unreadCountChanged) .or(extraDataChanged) .or(typingUsersChanged) + .or(draftMessageChanged) public operator fun plus(other: ChannelListPayloadDiff): ChannelListPayloadDiff = copy( @@ -46,5 +48,6 @@ public data class ChannelListPayloadDiff( unreadCountChanged = unreadCountChanged || other.unreadCountChanged, extraDataChanged = extraDataChanged || other.extraDataChanged, typingUsersChanged = typingUsersChanged || other.typingUsersChanged, + draftMessageChanged = draftMessageChanged || other.draftMessageChanged, ) } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/internal/ChannelListItemAdapter.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/internal/ChannelListItemAdapter.kt index 07a36272226..1d4a4a5ed61 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/internal/ChannelListItemAdapter.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/internal/ChannelListItemAdapter.kt @@ -76,6 +76,7 @@ internal class ChannelListItemAdapter( unreadCountChanged = true, extraDataChanged = true, typingUsersChanged = true, + draftMessageChanged = true, ) val EMPTY_CHANNEL_LIST_ITEM_PAYLOAD_DIFF: ChannelListPayloadDiff = ChannelListPayloadDiff( @@ -87,6 +88,7 @@ internal class ChannelListItemAdapter( unreadCountChanged = false, extraDataChanged = false, typingUsersChanged = false, + draftMessageChanged = false, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/internal/ChannelListItemDiffCallback.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/internal/ChannelListItemDiffCallback.kt index cf61b7ed2f2..24aea4cf5f1 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/internal/ChannelListItemDiffCallback.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/channels/list/adapter/internal/ChannelListItemDiffCallback.kt @@ -71,6 +71,7 @@ internal object ChannelListItemDiffCallback : DiffUtil.ItemCallback, + channelItem: ChannelListItem.ChannelItem, ) { channelNameLabel.text = ChatUI.channelNameFormatter.formatChannelName( channel = channel, currentUser = ChatUI.currentUserProvider.getCurrentUser(), ) - if (lastMessage != null || typingUsers.isNotEmpty()) { + if (lastMessage != null || channelItem.typingUsers.isNotEmpty() || channelItem.draftMessage != null) { channelNameLabel.translationY = 0f } else if (channelNameLabel.height > 0) { channelNameLabel.translationY = yDiffBetweenCenters(channelNameLabel, foregroundView) @@ -277,20 +290,24 @@ internal class ChannelViewHolder @JvmOverloads constructor( private fun StreamUiChannelListItemForegroundViewBinding.configureLastMessageLabelAndTimestamp( lastMessage: Message?, + draftMessage: DraftMessage?, ) { - lastMessageTimeLabel.isVisible = lastMessage.isNotNull() - - lastMessage ?: return run { - lastMessageLabel.text = "" - lastMessageTimeLabel.text = "" - } - - lastMessageLabel.text = ChatUI.messagePreviewFormatter.formatMessagePreview( - channel = channel, - message = lastMessage, - currentUser = ChatUI.currentUserProvider.getCurrentUser(), - ) - lastMessageTimeLabel.text = ChatUI.dateFormatter.formatDate(channel.lastMessageAt) + lastMessageTimeLabel.isVisible = lastMessage.isNotNull().and(draftMessage == null) + lastMessageTimeLabel.text = + lastMessage + ?.takeUnless { draftMessage != null } + ?.let { ChatUI.dateFormatter.formatDate(channel.lastMessageAt) } + ?: "" + lastMessageLabel.text = + draftMessage?.text + ?: lastMessage?.let { + ChatUI.messagePreviewFormatter.formatMessagePreview( + channel = channel, + message = it, + currentUser = ChatUI.currentUserProvider.getCurrentUser(), + ) + } + ?: "" } private fun StreamUiChannelListItemForegroundViewBinding.configureUnreadCountBadge() { @@ -369,6 +386,7 @@ internal class ChannelViewHolder @JvmOverloads constructor( height = style.itemHeight } channelNameLabel.setTextStyle(style.channelTitleText) + draftMessageLabel.setTextStyle(style.draftMessageLabel) lastMessageLabel.setTextStyle(style.lastMessageText) lastMessageTimeLabel.setTextStyle(style.lastMessageDateText) unreadCountBadge.setTextStyle(style.unreadMessageCounterText) diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModel.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModel.kt index 938bab92738..b605ba975f6 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModel.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModel.kt @@ -29,6 +29,7 @@ import io.getstream.chat.android.client.errors.extractCause import io.getstream.chat.android.client.extensions.cidToTypeAndId import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.ChannelMute +import io.getstream.chat.android.models.DraftMessage import io.getstream.chat.android.models.FilterObject import io.getstream.chat.android.models.Filters import io.getstream.chat.android.models.TypingEvent @@ -72,6 +73,7 @@ import kotlinx.coroutines.launch * @param limit The maximum number of channels to fetch. * @param messageLimit The number of messages to fetch for each channel. * @param memberLimit The number of members to fetch per channel. + * @param isDraftMessagesEnabled Enables or disables draft messages. * @param chatEventHandlerFactory The instance of [ChatEventHandlerFactory] that will be used to create [ChatEventHandler]. * @param chatClient Entry point for all low-level operations. * @param globalState Global state of OfflinePlugin. Contains information @@ -83,6 +85,7 @@ public class ChannelListViewModel( private val limit: Int = 30, private val messageLimit: Int = 1, private val memberLimit: Int = 30, + private val isDraftMessagesEnabled: Boolean, private val chatEventHandlerFactory: ChatEventHandlerFactory = ChatEventHandlerFactory(), private val chatClient: ChatClient = ChatClient.instance(), private val globalState: GlobalState = chatClient.globalState, @@ -109,6 +112,15 @@ public class ChannelListViewModel( public val typingEvents: LiveData> get() = globalState.typingChannels.asLiveData() + /** + * Draft messages for channels. + */ + public val draftMessages: LiveData> + get() = globalState.channelDraftMessages + .takeIf { isDraftMessagesEnabled } + ?.asLiveData() + ?: MutableLiveData(emptyMap()) + /** * Represents the current pagination state that is a product * of multiple sources. diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelBinding.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelBinding.kt index dc15876425b..aa5535c31f6 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelBinding.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelBinding.kt @@ -20,12 +20,13 @@ package io.getstream.chat.android.ui.viewmodel.channels import android.app.AlertDialog import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.distinctUntilChanged import io.getstream.chat.android.state.utils.EventObserver import io.getstream.chat.android.ui.R import io.getstream.chat.android.ui.feature.channels.list.ChannelListView import io.getstream.chat.android.ui.feature.channels.list.adapter.ChannelListItem -import io.getstream.chat.android.ui.utils.extensions.combineWith +import io.getstream.chat.android.ui.viewmodel.channels.internal.ChannelListBindingData /** * Binds [ChannelListView] with [ChannelListViewModel], updating the view's state based on @@ -35,40 +36,50 @@ import io.getstream.chat.android.ui.utils.extensions.combineWith * before setting any additional listeners on these objects yourself. */ @JvmName("bind") +@Suppress("LongMethod") public fun ChannelListViewModel.bindView( view: ChannelListView, lifecycleOwner: LifecycleOwner, ) { - state.combineWith(paginationState) { state, paginationState -> state to paginationState } - .combineWith(typingEvents) { states, typingEvents -> - val state = states?.first - val paginationState = states?.second - - paginationState?.let { - view.setPaginationEnabled(!it.endOfChannels && !it.loadingMore) - } - - var list: List = state?.channels?.map { - ChannelListItem.ChannelItem(it, typingEvents?.get(it.cid)?.users ?: emptyList()) - } ?: emptyList() - if (paginationState?.loadingMore == true) { - list = list + ChannelListItem.LoadingMoreItem - } + val mediatorLiveData = MediatorLiveData(ChannelListBindingData()) + mediatorLiveData.addSource(state) { + mediatorLiveData.value = mediatorLiveData.value?.copy(state = it) + } + mediatorLiveData.addSource(paginationState) { + mediatorLiveData.value = mediatorLiveData.value?.copy(paginationState = it) + } + mediatorLiveData.addSource(typingEvents) { + mediatorLiveData.value = mediatorLiveData.value?.copy(typingEvents = it) + } + mediatorLiveData.addSource(draftMessages) { + mediatorLiveData.value = mediatorLiveData.value?.copy(draftMessages = it) + } + mediatorLiveData + .distinctUntilChanged() + .observe(lifecycleOwner) { + with(it) { + view.setPaginationEnabled(!paginationState.endOfChannels && !paginationState.loadingMore) - list to (state?.isLoading == true) - }.distinctUntilChanged().observe(lifecycleOwner) { (list, isLoading) -> + val list: List = state.channels.map { channel -> + ChannelListItem.ChannelItem( + channel = channel, + typingUsers = typingEvents[channel.cid]?.users ?: emptyList(), + draftMessage = draftMessages[channel.cid], + ) + } + listOfNotNull(ChannelListItem.LoadingMoreItem.takeIf { paginationState.loadingMore }) - when { - isLoading && list.isEmpty() -> view.showLoadingView() + when { + state.isLoading && list.isEmpty() -> view.showLoadingView() - list.isNotEmpty() -> { - view.hideLoadingView() - view.setChannels(list) - } + list.isNotEmpty() -> { + view.hideLoadingView() + view.setChannels(list) + } - else -> { - view.hideLoadingView() - view.setChannels(emptyList()) + else -> { + view.hideLoadingView() + view.setChannels(emptyList()) + } } } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelFactory.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelFactory.kt index 644b8e1db75..241644f4ad5 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelFactory.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/ChannelListViewModelFactory.kt @@ -23,6 +23,7 @@ import io.getstream.chat.android.models.FilterObject import io.getstream.chat.android.models.Filters import io.getstream.chat.android.models.querysort.QuerySorter import io.getstream.chat.android.state.event.handler.chat.factory.ChatEventHandlerFactory +import io.getstream.chat.android.ui.ChatUI /** * Creates a channels view model factory. @@ -32,6 +33,7 @@ import io.getstream.chat.android.state.event.handler.chat.factory.ChatEventHandl * @param limit How many channels to return. * @param memberLimit The number of members per channel. * @param messageLimit The number of messages to fetch for each channel. + * @param isDraftMessagesEnabled Enables or disables draft messages. * @param chatEventHandlerFactory The instance of [ChatEventHandlerFactory] that will be used to create [ChatEventHandler]. * * @see Filters @@ -43,6 +45,7 @@ public class ChannelListViewModelFactory @JvmOverloads constructor( private val limit: Int = ChannelListViewModel.DEFAULT_CHANNEL_LIMIT, private val messageLimit: Int = ChannelListViewModel.DEFAULT_MESSAGE_LIMIT, private val memberLimit: Int = ChannelListViewModel.DEFAULT_MEMBER_LIMIT, + private val isDraftMessagesEnabled: Boolean = ChatUI.draftMessagesEnabled, private val chatEventHandlerFactory: ChatEventHandlerFactory = ChatEventHandlerFactory(), ) : ViewModelProvider.Factory { @@ -62,6 +65,7 @@ public class ChannelListViewModelFactory @JvmOverloads constructor( messageLimit = messageLimit, memberLimit = memberLimit, chatEventHandlerFactory = chatEventHandlerFactory, + isDraftMessagesEnabled = isDraftMessagesEnabled, ) as T } @@ -76,6 +80,7 @@ public class ChannelListViewModelFactory @JvmOverloads constructor( private var messageLimit: Int = ChannelListViewModel.DEFAULT_MESSAGE_LIMIT private var memberLimit: Int = ChannelListViewModel.DEFAULT_MEMBER_LIMIT private var chatEventHandlerFactory: ChatEventHandlerFactory = ChatEventHandlerFactory() + private var isDraftMessagesEnabled: Boolean = ChatUI.draftMessagesEnabled /** * Sets the way to filter the channels. @@ -119,6 +124,13 @@ public class ChannelListViewModelFactory @JvmOverloads constructor( this.chatEventHandlerFactory = chatEventHandlerFactory } + /** + * Enables or disables draft messages. + */ + public fun isDraftMessagesEnabled(isDraftMessagesEnabled: Boolean): Builder = apply { + this.isDraftMessagesEnabled = isDraftMessagesEnabled + } + /** * Builds [ChannelListViewModelFactory] instance. */ @@ -129,7 +141,8 @@ public class ChannelListViewModelFactory @JvmOverloads constructor( limit = limit, messageLimit = messageLimit, memberLimit = memberLimit, - chatEventHandlerFactory, + chatEventHandlerFactory = chatEventHandlerFactory, + isDraftMessagesEnabled = isDraftMessagesEnabled, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/internal/ChannelListBindingData.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/internal/ChannelListBindingData.kt new file mode 100644 index 00000000000..a3c5516fbcf --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/channels/internal/ChannelListBindingData.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014-2025 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.ui.viewmodel.channels.internal + +import io.getstream.chat.android.models.DraftMessage +import io.getstream.chat.android.models.TypingEvent +import io.getstream.chat.android.ui.viewmodel.channels.ChannelListViewModel + +internal data class ChannelListBindingData( + val state: ChannelListViewModel.State = ChannelListViewModel.State(false, emptyList()), + val paginationState: ChannelListViewModel.PaginationState = ChannelListViewModel.PaginationState(), + val typingEvents: Map = emptyMap(), + val draftMessages: Map = emptyMap(), +) diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelFactory.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelFactory.kt index a12d37d6093..32bf86bd31b 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelFactory.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelFactory.kt @@ -25,6 +25,7 @@ import io.getstream.chat.android.client.channel.state.ChannelState import io.getstream.chat.android.client.setup.state.ClientState import io.getstream.chat.android.models.Message import io.getstream.chat.android.state.extensions.watchChannelAsState +import io.getstream.chat.android.ui.ChatUI import io.getstream.chat.android.ui.common.feature.messages.composer.MessageComposerController import io.getstream.chat.android.ui.common.feature.messages.composer.mention.CompatUserLookupHandler import io.getstream.chat.android.ui.common.feature.messages.composer.mention.DefaultUserLookupHandler @@ -66,6 +67,7 @@ import java.io.File * @param showThreadSeparatorInEmptyThread Configures if we show a thread separator when threads are empty. * Adds the separator item when the value is `true`. * @param threadLoadOlderToNewer Configures if the thread should be loaded from older to newer messages. + * @param isComposerDraftMessagesEnabled Configures if the composer should support draft messages. * * @see MessageListHeaderViewModel * @see MessageListViewModel @@ -94,6 +96,7 @@ public class MessageListViewModelFactory @JvmOverloads constructor( private val showDateSeparatorInEmptyThread: Boolean = false, private val showThreadSeparatorInEmptyThread: Boolean = false, private val threadLoadOlderToNewer: Boolean = false, + private val isComposerDraftMessagesEnabled: Boolean = ChatUI.draftMessagesEnabled, ) : ViewModelProvider.Factory { private val channelStateFlow: StateFlow by lazy { @@ -138,9 +141,12 @@ public class MessageListViewModelFactory @JvmOverloads constructor( chatClient = chatClient, mediaRecorder = mediaRecorder, userLookupHandler = userLookupHandler, - maxAttachmentCount = maxAttachmentCount, fileToUri = fileToUri, channelState = channelStateFlow, + config = MessageComposerController.Config( + maxAttachmentCount = maxAttachmentCount, + isDraftMessageEnabled = isComposerDraftMessagesEnabled, + ), ), ) }, diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_channel_list_item_foreground_view.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_channel_list_item_foreground_view.xml index 4b35e7924f8..abe9f47bb59 100644 --- a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_channel_list_item_foreground_view.xml +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_channel_list_item_foreground_view.xml @@ -74,6 +74,19 @@ tools:text="Gebruiker, Usuario, Benutzer" /> + + + + diff --git a/stream-chat-android-ui-components/src/main/res/values/strings_channel_list.xml b/stream-chat-android-ui-components/src/main/res/values/strings_channel_list.xml index 8b97febe9c0..4a66eaf3ee5 100644 --- a/stream-chat-android-ui-components/src/main/res/values/strings_channel_list.xml +++ b/stream-chat-android-ui-components/src/main/res/values/strings_channel_list.xml @@ -37,4 +37,5 @@ Failed to hide channel Failed to delete channel Failed to leave channel + Draft: diff --git a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/channels/ChannelListViewModelTest.kt b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/channels/ChannelListViewModelTest.kt index 1d74186b865..cdcd9773dfa 100644 --- a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/channels/ChannelListViewModelTest.kt +++ b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/channels/ChannelListViewModelTest.kt @@ -311,6 +311,7 @@ internal class ChannelListViewModelTest { chatClient = chatClient, sort = initialSort, filter = initialFilters, + isDraftMessagesEnabled = false, chatEventHandlerFactory = ChatEventHandlerFactory( clientState = clientState, ), diff --git a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt index 2145b206838..47ce9b2c9e8 100644 --- a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt +++ b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageComposerViewModelTest.kt @@ -439,7 +439,10 @@ internal class MessageComposerViewModelTest { mediaRecorder = mock(), userLookupHandler = DefaultUserLookupHandler(chatClient, channelId), fileToUri = { it.path }, - maxAttachmentCount = maxAttachmentCount, + globalState = globalState, + config = MessageComposerController.Config( + maxAttachmentCount = maxAttachmentCount, + ), channelState = MutableStateFlow(channelState), ), )