diff --git a/stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt b/stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt index 7f67aec3ae2..a2fb459d835 100644 --- a/stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt +++ b/stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt @@ -921,6 +921,22 @@ public fun randomOption( text = text, ) +public fun randomVote( + id: String = randomString(), + pollId: String = randomString(), + optionId: String = randomString(), + createdAt: Date = randomDate(), + updatedAt: Date = randomDate(), + user: User? = randomUser(), +): Vote = Vote( + id = id, + pollId = pollId, + optionId = optionId, + createdAt = createdAt, + updatedAt = updatedAt, + user = user, +) + public fun randomPollOption( id: String? = randomString(), text: String = randomString(), 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 9d4a2965367..d99c0b19ca0 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 @@ -847,6 +847,7 @@ public final class io/getstream/chat/android/ui/common/feature/messages/list/Mes public fun (Ljava/lang/String;Lio/getstream/chat/android/ui/common/helper/ClipboardHandler;ZLjava/lang/String;Ljava/lang/String;ILio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lkotlinx/coroutines/flow/StateFlow;ZLio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;ZLio/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 synthetic fun (Ljava/lang/String;Lio/getstream/chat/android/ui/common/helper/ClipboardHandler;ZLjava/lang/String;Ljava/lang/String;ILio/getstream/chat/android/client/ChatClient;Lio/getstream/chat/android/client/setup/state/ClientState;Lkotlinx/coroutines/flow/StateFlow;ZLio/getstream/chat/android/ui/common/state/messages/list/MessageFooterVisibility;ZLio/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;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addPollOption (Lio/getstream/chat/android/models/Poll;Ljava/lang/String;)V + public final fun addPollOption (Ljava/lang/String;Ljava/lang/String;)V public final fun banUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V public static synthetic fun banUser$default (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)V public final fun blockUser (Ljava/lang/String;)V diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt index 30b77311e00..073de084961 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt @@ -2421,8 +2421,18 @@ public class MessageListController( * @param option The text of the new option to be added. */ public fun addPollOption(poll: Poll, option: String) { + addPollOption(pollId = poll.id, option = option) + } + + /** + * Creates a new poll option for the poll identified by [pollId]. + * + * @param pollId The id of the poll to which the option will be added. + * @param option The text of the new option to be added. + */ + public fun addPollOption(pollId: String, option: String) { scope.launch { - chatClient.createPollOption(poll.id, PollOption(text = option)).await() + chatClient.createPollOption(pollId, PollOption(text = option)).await() } } diff --git a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListControllerTests.kt b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListControllerTests.kt index 17634343994..6200ebf7af7 100644 --- a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListControllerTests.kt +++ b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListControllerTests.kt @@ -39,6 +39,7 @@ import io.getstream.chat.android.models.Member import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.MessageType import io.getstream.chat.android.models.MessagesState +import io.getstream.chat.android.models.PollOption import io.getstream.chat.android.models.Reaction import io.getstream.chat.android.models.TypingEvent import io.getstream.chat.android.models.User @@ -52,6 +53,7 @@ import io.getstream.chat.android.randomMembers import io.getstream.chat.android.randomMessage import io.getstream.chat.android.randomMessageList import io.getstream.chat.android.randomOption +import io.getstream.chat.android.randomPoll import io.getstream.chat.android.randomPollVote import io.getstream.chat.android.randomReaction import io.getstream.chat.android.randomString @@ -938,6 +940,21 @@ internal class MessageListControllerTests { controller.errorEvents.value `should be equal to` expectedEvent } + @Test + fun `When calling addPollOption, ChatClient createPollOption is invoked`() = runTest { + val poll = randomPoll() + val optionText = randomString() + val chatClient = mock() + val controller = Fixture(chatClient = chatClient) + .givenCurrentUser() + .givenChannelState(messagesState = MutableStateFlow(emptyList())) + .givenCreatePollOption(callFrom { PollOption(text = optionText) }) + .get() + controller.addPollOption(poll = poll, option = optionText) + + verify(chatClient).createPollOption(poll.id, PollOption(text = optionText)) + } + @Test fun `When toggleOriginalText, the message translation is toggled`() = runTest { val messageId = randomString() @@ -1281,6 +1298,10 @@ internal class MessageListControllerTests { whenever(chatClient.removePollVote(any(), any(), voteId = any())) doReturn vote } + fun givenCreatePollOption(option: Call) = apply { + whenever(chatClient.createPollOption(any(), any())) doReturn option + } + fun givenSendReaction(reaction: Call) = apply { whenever(chatClient.sendReaction(any(), any(), any(), any())) doReturn reaction } 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 d50fd9d9964..e7d76ec8085 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 @@ -15,6 +15,7 @@ public final class io/getstream/chat/android/ui/ChatUI { public static final fun getMessageTextTransformer ()Lio/getstream/chat/android/ui/helper/transformer/ChatMessageTextTransformer; public static final fun getMimeTypeIconProvider ()Lio/getstream/chat/android/ui/helper/MimeTypeIconProvider; public static final fun getNavigator ()Lio/getstream/chat/android/ui/navigation/ChatNavigator; + public static final fun getPollsConfig ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig; public static final fun getQuotedAttachmentFactoryManager ()Lio/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/attachment/QuotedAttachmentFactoryManager; public static final fun getReactionPushEmojiFactory ()Lio/getstream/chat/android/ui/common/helper/ReactionPushEmojiFactory; public static final fun getShowOriginalTranslationEnabled ()Z @@ -38,6 +39,7 @@ public final class io/getstream/chat/android/ui/ChatUI { public static final fun setMessageTextTransformer (Lio/getstream/chat/android/ui/helper/transformer/ChatMessageTextTransformer;)V public static final fun setMimeTypeIconProvider (Lio/getstream/chat/android/ui/helper/MimeTypeIconProvider;)V public static final fun setNavigator (Lio/getstream/chat/android/ui/navigation/ChatNavigator;)V + public static final fun setPollsConfig (Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig;)V public static final fun setQuotedAttachmentFactoryManager (Lio/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/attachment/QuotedAttachmentFactoryManager;)V public static final fun setReactionPushEmojiFactory (Lio/getstream/chat/android/ui/common/helper/ReactionPushEmojiFactory;)V public static final fun setShowOriginalTranslationEnabled (Z)V @@ -1491,6 +1493,8 @@ public final class io/getstream/chat/android/ui/feature/messages/composer/attach public final class io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment$Companion { public final fun newInstance (Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment$CreatePollDialogListener;)Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment; + public final fun newInstance (Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment$CreatePollDialogListener;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig;)Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment; + public static synthetic fun newInstance$default (Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment$Companion;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment$CreatePollDialogListener;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig;ILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment; } public abstract interface class io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment$CreatePollDialogListener { @@ -1508,6 +1512,7 @@ public final class io/getstream/chat/android/ui/feature/messages/composer/attach public final fun getPollIsReady ()Lkotlinx/coroutines/flow/StateFlow; public final fun onOptionTextChanged (ILjava/lang/String;)V public final fun onTitleChanged (Ljava/lang/String;)V + public final fun setAllowAnswers (Z)V public final fun setAllowMultipleVotes (Z)V public final fun setAnnonymousPoll (Z)V public final fun setMaxAnswer (Ljava/lang/Integer;)V @@ -1515,6 +1520,7 @@ public final class io/getstream/chat/android/ui/feature/messages/composer/attach } public final class io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/OptionsAdapter : androidx/recyclerview/widget/ListAdapter { + public fun (Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;)V public fun (Lkotlin/jvm/functions/Function2;)V public fun getItemId (I)J public synthetic fun onBindViewHolder (Landroidx/recyclerview/widget/RecyclerView$ViewHolder;I)V @@ -1524,8 +1530,8 @@ public final class io/getstream/chat/android/ui/feature/messages/composer/attach } public final class io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/OptionsAdapter$OptionViewHolder : androidx/recyclerview/widget/RecyclerView$ViewHolder { - public fun (Landroid/view/ViewGroup;Lio/getstream/chat/android/ui/databinding/StreamUiPollOptionBinding;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Landroid/view/ViewGroup;Lio/getstream/chat/android/ui/databinding/StreamUiPollOptionBinding;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroid/view/ViewGroup;Lio/getstream/chat/android/ui/databinding/StreamUiPollOptionBinding;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;)V + public synthetic fun (Landroid/view/ViewGroup;Lio/getstream/chat/android/ui/databinding/StreamUiPollOptionBinding;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun bind (Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollAnswer;)V } @@ -1544,6 +1550,75 @@ public final class io/getstream/chat/android/ui/feature/messages/composer/attach public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig : android/os/Parcelable { + public static final field CREATOR Landroid/os/Parcelable$Creator; + public static final field Companion Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig$Companion; + public fun (ZZ)V + public final fun component1 ()Z + public final fun component2 ()Z + public final fun copy (ZZ)Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;ZZILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public final fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getConfigurable ()Z + public final fun getDefaultValue ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig$Companion { + public final fun getDefault ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public final fun getNotConfigurable ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; +} + +public final class io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig : android/os/Parcelable { + public static final field CREATOR Landroid/os/Parcelable$Creator; + public static final field Companion Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig$Companion; + public fun ()V + public fun (Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Ljava/lang/Integer;Ljava/lang/Integer;)V + public synthetic fun (Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public final fun component2 ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public final fun component3 ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public final fun component4 ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public final fun component5 ()Ljava/lang/Integer; + public final fun component6 ()Ljava/lang/Integer; + public final fun copy (Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Ljava/lang/Integer;Ljava/lang/Integer;)Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig; + public final fun describeContents ()I + public fun equals (Ljava/lang/Object;)Z + public final fun getAllowComments ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public final fun getAnonymousPoll ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public final fun getMultipleVotes ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public final fun getOptionTextLimit ()Ljava/lang/Integer; + public final fun getQuestionTextLimit ()Ljava/lang/Integer; + public final fun getSuggestAnOption ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public final fun writeToParcel (Landroid/os/Parcel;I)V +} + +public final class io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig$Companion { + public final fun getDefault ()Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig; +} + +public final class io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lio/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class io/getstream/chat/android/ui/feature/messages/composer/attachment/preview/AttachmentPreviewFactoryManager { public fun ()V public fun (Ljava/util/List;)V @@ -2234,6 +2309,7 @@ public final class io/getstream/chat/android/ui/feature/messages/list/MessageLis public final fun setMessageViewHolderFactory (Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemViewHolderFactory;)V public final fun setModeratedMessageHandler (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$ModeratedMessageOptionHandler;)V public final fun setNewMessagesBehaviour (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$NewMessagesBehaviour;)V + public final fun setOnAddPollCommentClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnAddPollCommentClickListener;)V public final fun setOnAttachmentClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnAttachmentClickListener;)V public final fun setOnAttachmentDownloadClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnAttachmentDownloadClickListener;)V public final fun setOnEnterThreadListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnEnterThreadListener;)V @@ -2249,11 +2325,13 @@ public final class io/getstream/chat/android/ui/feature/messages/list/MessageLis public final fun setOnReactionViewClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnReactionViewClickListener;)V public final fun setOnReplyMessageClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnReplyMessageClickListener;)V public final fun setOnScrollToBottomHandler (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnScrollToBottomHandler;)V + public final fun setOnSuggestPollOptionClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnSuggestPollOptionClickListener;)V public final fun setOnThreadClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnThreadClickListener;)V public final fun setOnUnreadLabelClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnUnreadLabelClickListener;)V public final fun setOnUnreadLabelReachedListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnUnreadLabelReachedListener;)V public final fun setOnUserClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnUserClickListener;)V public final fun setOnUserReactionClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnUserReactionClickListener;)V + public final fun setOnViewPollCommentsClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnViewPollCommentsClickListener;)V public final fun setOnViewPollResultClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnViewPollResultClickListener;)V public final fun setOpenThreadHandler (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OpenThreadHandler;)V public final fun setOwnCapabilities (Ljava/util/Set;)V @@ -2387,6 +2465,10 @@ public final class io/getstream/chat/android/ui/feature/messages/list/MessageLis public static fun values ()[Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$NewMessagesBehaviour; } +public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$OnAddPollCommentClickListener { + public abstract fun onAddPollCommentClick (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Poll;)Z +} + public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$OnAttachmentClickListener { public abstract fun onAttachmentClick (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Attachment;)Z } @@ -2455,6 +2537,10 @@ public abstract interface class io/getstream/chat/android/ui/feature/messages/li public abstract fun onShowAllPollOptionClick (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Poll;)Z } +public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$OnSuggestPollOptionClickListener { + public abstract fun onSuggestPollOptionClick (Lio/getstream/chat/android/models/Poll;)Z +} + public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$OnThreadClickListener { public abstract fun onThreadClick (Lio/getstream/chat/android/models/Message;)Z } @@ -2479,6 +2565,10 @@ public abstract interface class io/getstream/chat/android/ui/feature/messages/li public abstract fun onUserReactionClick (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;Lio/getstream/chat/android/models/Reaction;)Z } +public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$OnViewPollCommentsClickListener { + public abstract fun onViewPollCommentsClick (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Poll;)Z +} + public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$OnViewPollResultClickListener { public abstract fun onViewPollResultClick (Lio/getstream/chat/android/models/Poll;)Z } @@ -2971,9 +3061,12 @@ public abstract interface class io/getstream/chat/android/ui/feature/messages/li public abstract fun getMessageClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageClickListener; public abstract fun getMessageLongClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageLongClickListener; public abstract fun getMessageRetryListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageRetryListener; + public abstract fun getOnAddPollCommentClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnAddPollCommentClickListener; public abstract fun getOnPollCloseClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnPollCloseClickListener; public abstract fun getOnPollOptionClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnPollOptionClickListener; public abstract fun getOnShowAllPollOptionClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnShowAllPollOptionClickListener; + public abstract fun getOnSuggestPollOptionClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnSuggestPollOptionClickListener; + public abstract fun getOnViewPollCommentsClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnViewPollCommentsClickListener; public abstract fun getOnViewPollResultClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnViewPollResultClickListener; public abstract fun getReactionViewClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnReactionViewClickListener; public abstract fun getThreadClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnThreadClickListener; @@ -3054,8 +3147,11 @@ public final class io/getstream/chat/android/ui/feature/messages/list/adapter/vi } public final class io/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle : io/getstream/chat/android/ui/helper/ViewStyle { - public fun (Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/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;Lio/getstream/chat/android/ui/font/TextStyle;)V + public fun (Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/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;Lio/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;)V + public synthetic fun (Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/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;Lio/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;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun component10 ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun component11 ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun component2 ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun component3 ()Landroid/graphics/drawable/Drawable; public final fun component4 ()Lio/getstream/chat/android/ui/font/TextStyle; @@ -3063,9 +3159,11 @@ public final class io/getstream/chat/android/ui/feature/messages/list/adapter/vi public final fun component6 ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun component7 ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun component8 ()Lio/getstream/chat/android/ui/font/TextStyle; - public final fun copy (Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/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;Lio/getstream/chat/android/ui/font/TextStyle;)Lio/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/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;Lio/getstream/chat/android/ui/font/TextStyle;ILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle; + public final fun component9 ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun copy (Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/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;Lio/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;)Lio/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle;Lio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;Landroid/graphics/drawable/Drawable;Lio/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;Lio/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;ILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle; public fun equals (Ljava/lang/Object;)Z + public final fun getPollAddCommentTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun getPollCloseTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun getPollOptionCheckDrawable ()Landroid/graphics/drawable/Drawable; public final fun getPollOptionTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; @@ -3073,7 +3171,9 @@ public final class io/getstream/chat/android/ui/feature/messages/list/adapter/vi public final fun getPollResultsTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun getPollShowAllOptionsTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun getPollSubtitleTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun getPollSuggestOptionTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun getPollTitleTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; + public final fun getPollViewCommentsTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -4811,6 +4911,21 @@ public final class io/getstream/chat/android/ui/viewmodel/messages/MessageListVi public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$PollAnswerCast : io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$PollAnswerCast; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$PollAnswerCast;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$PollAnswerCast; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnswer ()Ljava/lang/String; + public final fun getMessageId ()Ljava/lang/String; + public final fun getPollId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$PollClosed : io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event { public fun (Lio/getstream/chat/android/models/Poll;)V public final fun component1 ()Lio/getstream/chat/android/models/Poll; @@ -4822,6 +4937,19 @@ public final class io/getstream/chat/android/ui/viewmodel/messages/MessageListVi public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$PollOptionSuggested : io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$PollOptionSuggested; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$PollOptionSuggested;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$PollOptionSuggested; + public fun equals (Ljava/lang/Object;)Z + public final fun getOption ()Ljava/lang/String; + public final fun getPollId ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$PollOptionUpdated : io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event { public fun (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Option;)V public final fun component1 ()Lio/getstream/chat/android/models/Message; diff --git a/stream-chat-android-ui-components/build.gradle.kts b/stream-chat-android-ui-components/build.gradle.kts index b849a02a4b3..7c760674cee 100644 --- a/stream-chat-android-ui-components/build.gradle.kts +++ b/stream-chat-android-ui-components/build.gradle.kts @@ -90,6 +90,7 @@ dependencies { testImplementation(libs.kluent) testImplementation(libs.mockito.kotlin) testImplementation(libs.androidx.core.testing) + testImplementation(libs.turbine) detektPlugins(libs.detekt.formatting) 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 7d132032b77..f67f76b9532 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 @@ -23,6 +23,7 @@ import io.getstream.chat.android.ui.common.helper.DurationFormatter import io.getstream.chat.android.ui.common.helper.ReactionPushEmojiFactory import io.getstream.chat.android.ui.common.images.resizing.StreamCdnImageResizing import io.getstream.chat.android.ui.common.utils.ChannelNameFormatter +import io.getstream.chat.android.ui.feature.messages.composer.attachment.picker.poll.PollsConfig import io.getstream.chat.android.ui.feature.messages.composer.attachment.preview.AttachmentPreviewFactoryManager import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.AttachmentFactoryManager import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.DefaultQuotedAttachmentMessageFactory @@ -200,6 +201,15 @@ public object ChatUI { @JvmStatic public var showOriginalTranslationEnabled: Boolean = true + /** + * Configuration for poll creation features. Controls which poll features are configurable by the user + * and their default values. + * + * @see PollsConfig + */ + @JvmStatic + public var pollsConfig: PollsConfig = PollsConfig.Default + /** * Provides a custom renderer for user avatars. */ diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment.kt index 8c5f3714007..7a8ab7b7a8f 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment.kt @@ -17,6 +17,7 @@ package io.getstream.chat.android.ui.feature.messages.composer.attachment.picker.poll import android.os.Bundle +import android.text.InputFilter import android.view.LayoutInflater import android.view.MenuItem import android.view.View @@ -24,15 +25,18 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat +import androidx.core.os.BundleCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.core.widget.addTextChangedListener import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import io.getstream.chat.android.models.CreatePollParams +import io.getstream.chat.android.ui.ChatUI import io.getstream.chat.android.ui.R import io.getstream.chat.android.ui.common.utils.PollsConstants import io.getstream.chat.android.ui.databinding.StreamUiFragmentCreatePollBinding +import io.getstream.chat.android.ui.feature.messages.composer.attachment.picker.poll.CreatePollDialogFragment.Companion.newInstance import io.getstream.chat.android.ui.utils.extensions.applyEdgeToEdgePadding import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater import kotlinx.coroutines.flow.collectLatest @@ -41,6 +45,8 @@ import io.getstream.chat.android.ui.common.R as UiCommonR /** * Represent the bottom sheet dialog that allows users to pick attachments. + * + * Use [newInstance] to create an instance with optional [PollsConfig]. */ public class CreatePollDialogFragment : AppCompatDialogFragment() { @@ -48,8 +54,16 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() { private val binding get() = _binding!! private var createPollDialogListener: CreatePollDialogListener? = null private val createPollViewModel: CreatePollViewModel by viewModels() + private val pollsConfig: PollsConfig by lazy { + arguments?.let { + BundleCompat.getParcelable(it, ARG_POLLS_CONFIG, PollsConfig::class.java) + } ?: ChatUI.pollsConfig + } private val optionsAdapter: OptionsAdapter by lazy { - OptionsAdapter { id, text -> createPollViewModel.onOptionTextChanged(id, text) } + OptionsAdapter( + optionTextLimit = pollsConfig.optionTextLimit, + onOptionChange = { id, text -> createPollViewModel.onOptionTextChanged(id, text) }, + ) } private lateinit var sendMenuItem: MenuItem @@ -75,6 +89,52 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() { super.onViewCreated(view, savedInstanceState) binding.root.applyEdgeToEdgePadding(typeMask = WindowInsetsCompat.Type.systemBars()) setupDialog() + + if (savedInstanceState == null) { + applyFeatureDefaults() + } + applyFeatureVisibility() + } + + /** + * Applies the default state of poll features to the ViewModel and switches based on [pollsConfig]. + */ + private fun applyFeatureDefaults() { + createPollViewModel.setAllowMultipleVotes(pollsConfig.multipleVotes.defaultValue) + if (pollsConfig.multipleVotes.configurable) { + binding.multipleAnswersSwitch.isChecked = pollsConfig.multipleVotes.defaultValue + } + + createPollViewModel.setAnnonymousPoll(pollsConfig.anonymousPoll.defaultValue) + if (pollsConfig.anonymousPoll.configurable) { + binding.anonymousPollSwitch.isChecked = pollsConfig.anonymousPoll.defaultValue + } + + createPollViewModel.setSuggestAnOption(pollsConfig.suggestAnOption.defaultValue) + if (pollsConfig.suggestAnOption.configurable) { + binding.suggestAnOptionSwitch.isChecked = pollsConfig.suggestAnOption.defaultValue + } + + createPollViewModel.setAllowAnswers(pollsConfig.allowComments.defaultValue) + if (pollsConfig.allowComments.configurable) { + binding.addACommentLabelSwitch.isChecked = pollsConfig.allowComments.defaultValue + } + } + + private fun applyFeatureVisibility() { + binding.multipleAnswersLabel.isVisible = pollsConfig.multipleVotes.configurable + binding.multipleAnswersSwitch.isVisible = pollsConfig.multipleVotes.configurable + binding.multipleAnswersCount.isVisible = + pollsConfig.multipleVotes.configurable && binding.multipleAnswersSwitch.isChecked + + binding.anonymousPollLabel.isVisible = pollsConfig.anonymousPoll.configurable + binding.anonymousPollSwitch.isVisible = pollsConfig.anonymousPoll.configurable + + binding.suggestAnOptionLabel.isVisible = pollsConfig.suggestAnOption.configurable + binding.suggestAnOptionSwitch.isVisible = pollsConfig.suggestAnOption.configurable + + binding.addACommentLabel.isVisible = pollsConfig.allowComments.configurable + binding.addACommentLabelSwitch.isVisible = pollsConfig.allowComments.configurable } /** @@ -82,6 +142,10 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() { */ private fun setupDialog() { setupToolbar(binding.toolbar) + pollsConfig.questionTextLimit?.let { limit -> + binding.question.filters = arrayOf(InputFilter.LengthFilter(limit)) + } + binding.multipleAnswersSwitch.setOnCheckedChangeListener { _, isChecked -> binding.multipleAnswersCount.isVisible = isChecked createPollViewModel.setAllowMultipleVotes(isChecked) @@ -98,6 +162,9 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() { binding.suggestAnOptionSwitch.setOnCheckedChangeListener { _, isChecked -> createPollViewModel.setSuggestAnOption(isChecked) } + binding.addACommentLabelSwitch.setOnCheckedChangeListener { _, isChecked -> + createPollViewModel.setAllowAnswers(isChecked) + } binding.addOption.setOnClickListener { createPollViewModel.createOption() } @@ -159,15 +226,25 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() { public companion object { public const val TAG: String = "create_poll_dialog_fragment" + private const val ARG_POLLS_CONFIG: String = "arg_polls_config" /** * Creates a new instance of [CreatePollDialogFragment]. * + * @param createPollDialogListener The listener for poll creation events. + * @param pollsConfig Optional configuration for poll features. Defaults to [ChatUI.pollsConfig]. * @return A new instance of [CreatePollDialogFragment]. */ - public fun newInstance(createPollDialogListener: CreatePollDialogListener): CreatePollDialogFragment { - return CreatePollDialogFragment() - .setCreatePollDialogListener(createPollDialogListener) + @JvmOverloads + public fun newInstance( + createPollDialogListener: CreatePollDialogListener, + pollsConfig: PollsConfig? = null, + ): CreatePollDialogFragment { + return CreatePollDialogFragment().apply { + arguments = Bundle().apply { + pollsConfig?.let { config -> putParcelable(ARG_POLLS_CONFIG, config) } + } + }.setCreatePollDialogListener(createPollDialogListener) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollViewModel.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollViewModel.kt index 71c0ca4bf33..7c60a160a98 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollViewModel.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollViewModel.kt @@ -46,6 +46,7 @@ public class CreatePollViewModel : ViewModel() { private val createPoll = MutableSharedFlow(extraBufferCapacity = 1) private var suggestAnOption = false private var annonymousPoll = false + private var allowAnswers = false private var allowMultipleVotes = MutableStateFlow(false) private var maxAnswers: MutableStateFlow = MutableStateFlow(null) @@ -91,6 +92,7 @@ public class CreatePollViewModel : ViewModel() { options = options.map { PollOption(text = it.text) }, votingVisibility = if (annonymousPoll) VotingVisibility.ANONYMOUS else VotingVisibility.PUBLIC, allowUserSuggestedOptions = suggestAnOption, + allowAnswers = allowAnswers, maxVotesAllowed = maxAnswers.takeIf { allowMultipleVotes } ?: 1, enforceUniqueVote = !allowMultipleVotes, ) @@ -202,4 +204,13 @@ public class CreatePollViewModel : ViewModel() { public fun setAnnonymousPoll(annonymousPoll: Boolean) { this.annonymousPoll = annonymousPoll } + + /** + * Set if the poll allows users to add answers/comments. + * + * @param allowAnswers True if the poll allows users to add answers/comments, false otherwise. + */ + public fun setAllowAnswers(allowAnswers: Boolean) { + this.allowAnswers = allowAnswers + } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/OptionsAdapter.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/OptionsAdapter.kt index f6430e3b2a1..64b0325d3dc 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/OptionsAdapter.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/OptionsAdapter.kt @@ -17,6 +17,7 @@ package io.getstream.chat.android.ui.feature.messages.composer.attachment.picker.poll import android.text.Editable +import android.text.InputFilter import android.text.TextWatcher import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil @@ -27,9 +28,17 @@ import io.getstream.chat.android.ui.databinding.StreamUiPollOptionBinding import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater public class OptionsAdapter( + private val optionTextLimit: Int?, private val onOptionChange: (id: Int, text: String) -> Unit, ) : ListAdapter(OptionDiffCallback) { + /** + * Builds an [OptionsAdapter] instance without providing option text limit. + * + * @param onOptionChange Callback invoked when the option text changes. + */ + public constructor(onOptionChange: (id: Int, text: String) -> Unit) : this(null, onOptionChange) + init { setHasStableIds(true) } @@ -39,6 +48,7 @@ public class OptionsAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionViewHolder = OptionViewHolder( parent = parent, + optionTextLimit = optionTextLimit, onOptionChange = onOptionChange, ) @@ -53,11 +63,18 @@ public class OptionsAdapter( parent, false, ), + private val optionTextLimit: Int?, private val onOptionChange: (id: Int, text: String) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { private lateinit var pollAnswer: PollAnswer + init { + optionTextLimit?.let { limit -> + binding.option.filters = arrayOf(InputFilter.LengthFilter(limit)) + } + } + private val textWatcher = object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { /* no-op */ } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { /* no-op */ } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig.kt new file mode 100644 index 00000000000..40ebfb47d2a --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2014-2026 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.feature.messages.composer.attachment.picker.poll + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Configuration for individual poll entry feature. + * + * @param configurable Indicates whether the poll entry is configurable. When false, the UI element is hidden. + * @param defaultValue Indicates the default value of the poll entry. + */ +@Parcelize +public data class PollFeatureConfig( + val configurable: Boolean, + val defaultValue: Boolean, +) : Parcelable { + public companion object { + /** + * The default configuration for a poll entry. It will make it configurable and disabled by default. + */ + public val Default: PollFeatureConfig = PollFeatureConfig( + configurable = true, + defaultValue = false, + ) + + /** + * The feature should not be supported, so it is not configurable by the user and hidden from the UI. + */ + public val NotConfigurable: PollFeatureConfig = PollFeatureConfig( + configurable = false, + defaultValue = false, + ) + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig.kt new file mode 100644 index 00000000000..a9d9bda5c1d --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014-2026 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.feature.messages.composer.attachment.picker.poll + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * The configuration for the various poll features. It determines if the user can or cannot enable + * certain poll features. + * + * @param multipleVotes Configuration for allowing multiple votes in a poll. + * @param anonymousPoll Configuration for enabling anonymous polls. + * @param suggestAnOption Configuration for allowing users to suggest options in a poll. + * @param allowComments Configuration for adding comments to a poll. + * @param questionTextLimit Optional character limit for the poll question. Null means no limit. + * @param optionTextLimit Optional character limit for poll answer options. Null means no limit. + */ +@Parcelize +public data class PollsConfig( + val multipleVotes: PollFeatureConfig = PollFeatureConfig.Default, + val anonymousPoll: PollFeatureConfig = PollFeatureConfig.Default, + val suggestAnOption: PollFeatureConfig = PollFeatureConfig.Default, + val allowComments: PollFeatureConfig = PollFeatureConfig.Default, + val questionTextLimit: Int? = null, + val optionTextLimit: Int? = null, +) : Parcelable { + + init { + require(multipleVotes.configurable || !multipleVotes.defaultValue) { + "Invalid PollsConfig: multipleVotes cannot have defaultValue=true while " + + "configurable=false as the user would be unable to set maxVotesAllowed." + } + require(questionTextLimit == null || questionTextLimit > 0) { + "Invalid PollsConfig: questionTextLimit must be > 0 when provided." + } + require(optionTextLimit == null || optionTextLimit > 0) { + "Invalid PollsConfig: optionTextLimit must be > 0 when provided." + } + } + + public companion object { + /** + * The default configuration for polls. All features are configurable and disabled by default. + */ + public val Default: PollsConfig = PollsConfig() + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt index 6d03fc0aa2f..71b46e60cea 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt @@ -93,8 +93,11 @@ import io.getstream.chat.android.ui.feature.messages.list.internal.HiddenMessage import io.getstream.chat.android.ui.feature.messages.list.internal.MessageListScrollHelper import io.getstream.chat.android.ui.feature.messages.list.internal.SwipeReplyCallback import io.getstream.chat.android.ui.feature.messages.list.internal.canReplyToMessage +import io.getstream.chat.android.ui.feature.messages.list.internal.poll.AddPollCommentDialogFragment import io.getstream.chat.android.ui.feature.messages.list.internal.poll.AllPollOptionsDialogFragment +import io.getstream.chat.android.ui.feature.messages.list.internal.poll.PollCommentsDialogFragment import io.getstream.chat.android.ui.feature.messages.list.internal.poll.PollResultsDialogFragment +import io.getstream.chat.android.ui.feature.messages.list.internal.poll.SuggestPollOptionDialogFragment import io.getstream.chat.android.ui.feature.messages.list.options.message.MessageOptionItem import io.getstream.chat.android.ui.feature.messages.list.options.message.MessageOptionItemsFactory import io.getstream.chat.android.ui.feature.messages.list.options.message.MessageOptionsDialogFragment @@ -609,6 +612,28 @@ public class MessageListView : ConstraintLayout { true } ?: false } + private val defaultOnSuggestPollOptionClickListener = OnSuggestPollOptionClickListener { poll -> + context.getFragmentManager()?.let { fragmentManager -> + SuggestPollOptionDialogFragment.newInstance(poll.id) + .show(fragmentManager, SuggestPollOptionDialogFragment.TAG) + true + } ?: false + } + private val defaultOnAddPollCommentClickListener = OnAddPollCommentClickListener { message, poll -> + context.getFragmentManager()?.let { fragmentManager -> + AddPollCommentDialogFragment.newInstance(messageId = message.id, pollId = poll.id) + .show(fragmentManager, AddPollCommentDialogFragment.TAG) + true + } ?: false + } + private val defaultOnViewPollCommentsClickListener = OnViewPollCommentsClickListener { message, _ -> + context.getFragmentManager()?.let { fragmentManager -> + PollCommentsDialogFragment + .newInstance(cid = message.cid, messageId = message.id) + .show(fragmentManager, PollCommentsDialogFragment.TAG) + true + } ?: false + } private val listenerContainer = MessageListListenersImpl( messageClickListener = defaultMessageClickListener, @@ -626,6 +651,9 @@ public class MessageListView : ConstraintLayout { onShowAllPollOptionClickListener = defaultOnShowAllPollOptionClickListener, onPollCloseClickListener = defaultOnPollCloseClickListener, onViewPollResultClickListener = defaultOnViewPollResultClickListener, + onSuggestPollOptionClickListener = defaultOnSuggestPollOptionClickListener, + onAddPollCommentClickListener = defaultOnAddPollCommentClickListener, + onViewPollCommentsClickListener = defaultOnViewPollCommentsClickListener, ) private var enterThreadListener = defaultEnterThreadListener private var userReactionClickListener = defaultUserReactionClickListener @@ -656,9 +684,9 @@ public class MessageListView : ConstraintLayout { binding = StreamUiMessageListViewBinding.inflate(streamThemeInflater, this) messageListViewStyle = MessageListViewStyle(context, attr) - messageListViewStyle?.messagesStart?.let(::chatMessageStart) initRecyclerView() + messageListViewStyle?.messagesStart?.let(::chatMessageStart) initScrollHelper() initSwipeToReply() initLoadingView() @@ -704,9 +732,7 @@ public class MessageListView : ConstraintLayout { private fun initRecyclerView() { binding.chatMessagesRV.apply { - layoutManager = LinearLayoutManager(context).apply { - stackFromEnd = true - } + layoutManager = LinearLayoutManager(context) setHasFixedSize(false) setItemViewCacheSize(20) } @@ -1310,15 +1336,16 @@ public class MessageListView : ConstraintLayout { private fun changeLayoutForMessageStart(messagesStart: MessagesStart) { val messagesRV = binding.chatMessagesRV + val layoutManager = messagesRV.layoutManager as? LinearLayoutManager ?: return when (messagesStart) { MessagesStart.BOTTOM -> { - messagesRV.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + layoutManager.stackFromEnd = true messagesRV.overScrollMode = View.OVER_SCROLL_NEVER } MessagesStart.TOP -> { - messagesRV.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + layoutManager.stackFromEnd = false messagesRV.overScrollMode = View.OVER_SCROLL_ALWAYS } } @@ -1393,6 +1420,48 @@ public class MessageListView : ConstraintLayout { } } + /** + * Set the Suggest Poll Option click listener to be used by MessageListView. + * + * @param listener The listener to use. If null, the default will be used instead. + */ + public fun setOnSuggestPollOptionClickListener(listener: OnSuggestPollOptionClickListener?) { + listenerContainer.onSuggestPollOptionClickListener = + if (listener == null) { + defaultOnSuggestPollOptionClickListener + } else { + OnSuggestPollOptionClickListener(listener::onSuggestPollOptionClick) + } + } + + /** + * Set the Add Poll Comment click listener to be used by MessageListView. + * + * @param listener The listener to use. If null, the default will be used instead. + */ + public fun setOnAddPollCommentClickListener(listener: OnAddPollCommentClickListener?) { + listenerContainer.onAddPollCommentClickListener = + if (listener == null) { + defaultOnAddPollCommentClickListener + } else { + OnAddPollCommentClickListener(listener::onAddPollCommentClick) + } + } + + /** + * Set the View Poll Comments click listener to be used by MessageListView. + * + * @param listener The listener to use. If null, the default will be used instead. + */ + public fun setOnViewPollCommentsClickListener(listener: OnViewPollCommentsClickListener?) { + listenerContainer.onViewPollCommentsClickListener = + if (listener == null) { + defaultOnViewPollCommentsClickListener + } else { + OnViewPollCommentsClickListener(listener::onViewPollCommentsClick) + } + } + /** * Sets the message long click listener to be used by MessageListView. * @@ -2011,6 +2080,27 @@ public class MessageListView : ConstraintLayout { public fun onViewPollResultClick(poll: Poll): Boolean } + /** + * Listener for clicks on the "Suggest an option" button of a poll that allows user-suggested options. + */ + public fun interface OnSuggestPollOptionClickListener { + public fun onSuggestPollOptionClick(poll: Poll): Boolean + } + + /** + * Listener for clicks on the "Add a comment" button of a poll that allows answers. + */ + public fun interface OnAddPollCommentClickListener { + public fun onAddPollCommentClick(message: Message, poll: Poll): Boolean + } + + /** + * Listener for clicks on the "View comments" button of a poll that has answers. + */ + public fun interface OnViewPollCommentsClickListener { + public fun onViewPollCommentsClick(message: Message, poll: Poll): Boolean + } + public fun interface OnReplyMessageClickListener { public fun onReplyClick(replyTo: Message): Boolean } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListeners.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListeners.kt index e125dc8ef59..0e5b8ff9650 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListeners.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListeners.kt @@ -16,6 +16,7 @@ package io.getstream.chat.android.ui.feature.messages.list.adapter +import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAddPollCommentClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAttachmentClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAttachmentDownloadClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnGiphySendListener @@ -28,10 +29,12 @@ import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnPoll import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnPollOptionClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnReactionViewClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnShowAllPollOptionClickListener +import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnSuggestPollOptionClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnThreadClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnTranslatedLabelClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnUnreadLabelReachedListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnUserClickListener +import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnViewPollCommentsClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnViewPollResultClickListener public sealed interface MessageListListeners { @@ -52,4 +55,7 @@ public sealed interface MessageListListeners { public val onShowAllPollOptionClickListener: OnShowAllPollOptionClickListener public val onPollCloseClickListener: OnPollCloseClickListener public val onViewPollResultClickListener: OnViewPollResultClickListener + public val onSuggestPollOptionClickListener: OnSuggestPollOptionClickListener + public val onAddPollCommentClickListener: OnAddPollCommentClickListener + public val onViewPollCommentsClickListener: OnViewPollCommentsClickListener } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenersImpl.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenersImpl.kt index 8dd4e8a1a4e..fa8d37cd778 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenersImpl.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenersImpl.kt @@ -16,6 +16,7 @@ package io.getstream.chat.android.ui.feature.messages.list.adapter +import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAddPollCommentClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAttachmentClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAttachmentDownloadClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnGiphySendListener @@ -28,13 +29,16 @@ import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnPoll import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnPollOptionClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnReactionViewClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnShowAllPollOptionClickListener +import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnSuggestPollOptionClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnThreadClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnTranslatedLabelClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnUnreadLabelReachedListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnUserClickListener +import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnViewPollCommentsClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnViewPollResultClickListener import io.getstream.chat.android.ui.utils.ListenerDelegate +@Suppress("LongParameterList") internal class MessageListListenersImpl( messageClickListener: OnMessageClickListener = OnMessageClickListener(EmptyFunctions.ONE_PARAM), messageLongClickListener: OnMessageLongClickListener = OnMessageLongClickListener(EmptyFunctions.ONE_PARAM), @@ -57,6 +61,9 @@ internal class MessageListListenersImpl( onShowAllPollOptionClickListener: OnShowAllPollOptionClickListener, onPollCloseClickListener: OnPollCloseClickListener, onViewPollResultClickListener: OnViewPollResultClickListener, + onSuggestPollOptionClickListener: OnSuggestPollOptionClickListener, + onAddPollCommentClickListener: OnAddPollCommentClickListener, + onViewPollCommentsClickListener: OnViewPollCommentsClickListener, ) : MessageListListeners { private object EmptyFunctions { val ONE_PARAM: (Any) -> Boolean = { _ -> false } @@ -198,4 +205,28 @@ internal class MessageListListenersImpl( realListener().onViewPollResultClick(poll) } } + + override var onSuggestPollOptionClickListener: OnSuggestPollOptionClickListener by ListenerDelegate( + onSuggestPollOptionClickListener, + ) { realListener -> + OnSuggestPollOptionClickListener { poll -> + realListener().onSuggestPollOptionClick(poll) + } + } + + override var onAddPollCommentClickListener: OnAddPollCommentClickListener by ListenerDelegate( + onAddPollCommentClickListener, + ) { realListener -> + OnAddPollCommentClickListener { message, poll -> + realListener().onAddPollCommentClick(message, poll) + } + } + + override var onViewPollCommentsClickListener: OnViewPollCommentsClickListener by ListenerDelegate( + onViewPollCommentsClickListener, + ) { realListener -> + OnViewPollCommentsClickListener { message, poll -> + realListener().onViewPollCommentsClick(message, poll) + } + } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle.kt index 54d51dfced9..84bab3b804e 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle.kt @@ -38,6 +38,9 @@ public data class PollViewStyle( public val pollCloseTextStyle: TextStyle, public val pollResultsTextStyle: TextStyle, public val pollShowAllOptionsTextStyle: TextStyle, + public val pollSuggestOptionTextStyle: TextStyle = TextStyle(), + public val pollAddCommentTextStyle: TextStyle = TextStyle(), + public val pollViewCommentsTextStyle: TextStyle = TextStyle(), ) : ViewStyle { internal companion object { @@ -184,6 +187,63 @@ public data class PollViewStyle( Typeface.NORMAL, ) .build() + + val pollSuggestOptionTextStyle = TextStyle.Builder(a) + .size( + R.styleable.PollView_streamUiPollSuggestOptionTextSize, + context.getDimension(R.dimen.stream_ui_text_large), + ) + .color( + R.styleable.PollView_streamUiPollSuggestOptionTextColor, + context.getColorCompat(R.color.stream_ui_accent_blue), + ) + .font( + R.styleable.PollView_streamUiPollSuggestOptionFontAssets, + R.styleable.PollView_streamUiPollSuggestOptionTextFont, + ) + .style( + R.styleable.PollView_streamUiPollSuggestOptionTextStyle, + Typeface.NORMAL, + ) + .build() + + val pollAddCommentTextStyle = TextStyle.Builder(a) + .size( + R.styleable.PollView_streamUiPollAddCommentTextSize, + context.getDimension(R.dimen.stream_ui_text_large), + ) + .color( + R.styleable.PollView_streamUiPollAddCommentTextColor, + context.getColorCompat(R.color.stream_ui_accent_blue), + ) + .font( + R.styleable.PollView_streamUiPollAddCommentFontAssets, + R.styleable.PollView_streamUiPollAddCommentTextFont, + ) + .style( + R.styleable.PollView_streamUiPollAddCommentTextStyle, + Typeface.NORMAL, + ) + .build() + + val pollViewCommentsTextStyle = TextStyle.Builder(a) + .size( + R.styleable.PollView_streamUiPollViewCommentsTextSize, + context.getDimension(R.dimen.stream_ui_text_large), + ) + .color( + R.styleable.PollView_streamUiPollViewCommentsTextColor, + context.getColorCompat(R.color.stream_ui_accent_blue), + ) + .font( + R.styleable.PollView_streamUiPollViewCommentsFontAssets, + R.styleable.PollView_streamUiPollViewCommentsTextFont, + ) + .style( + R.styleable.PollView_streamUiPollViewCommentsTextStyle, + Typeface.NORMAL, + ) + .build() return PollViewStyle( pollTitleTextStyle = pollTitleTextStyle, pollSubtitleTextStyle = pollSubtitleTextStyle, @@ -193,6 +253,9 @@ public data class PollViewStyle( pollCloseTextStyle = pollCloseTextStyle, pollResultsTextStyle = pollResultsTextStyle, pollShowAllOptionsTextStyle = pollShowAllOptionsTextStyle, + pollSuggestOptionTextStyle = pollSuggestOptionTextStyle, + pollAddCommentTextStyle = pollAddCommentTextStyle, + pollViewCommentsTextStyle = pollViewCommentsTextStyle, ).let(TransformStyle.pollViewStyleTransformer::transform) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/internal/PollView.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/internal/PollView.kt index 6cf7a9907a2..f90fa743dd9 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/internal/PollView.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/internal/PollView.kt @@ -34,11 +34,14 @@ import io.getstream.chat.android.models.Vote import io.getstream.chat.android.ui.R import io.getstream.chat.android.ui.common.utils.PollsConstants import io.getstream.chat.android.ui.common.utils.extensions.getSubtitle +import io.getstream.chat.android.ui.databinding.StreamUiItemPollAddCommentBinding import io.getstream.chat.android.ui.databinding.StreamUiItemPollAnswerBinding import io.getstream.chat.android.ui.databinding.StreamUiItemPollCloseBinding import io.getstream.chat.android.ui.databinding.StreamUiItemPollHeaderBinding import io.getstream.chat.android.ui.databinding.StreamUiItemPollResultsBinding import io.getstream.chat.android.ui.databinding.StreamUiItemPollShowAllOptionsBinding +import io.getstream.chat.android.ui.databinding.StreamUiItemPollSuggestOptionBinding +import io.getstream.chat.android.ui.databinding.StreamUiItemPollViewCommentsBinding import io.getstream.chat.android.ui.feature.messages.list.adapter.view.PollViewStyle import io.getstream.chat.android.ui.font.setTextStyle import io.getstream.chat.android.ui.utils.extensions.createStreamThemeWrapper @@ -57,6 +60,9 @@ internal class PollView : RecyclerView { var onClosePollClick: ((Poll) -> Unit) = { _ -> } var onViewPollResultsClick: ((Poll) -> Unit) = { _ -> } var onShowAllPollOptionClick: (() -> Unit) = { } + var onSuggestOptionClick: ((Poll) -> Unit) = { _ -> } + var onAddCommentClick: ((Poll) -> Unit) = { _ -> } + var onViewCommentsClick: ((Poll) -> Unit) = { _ -> } constructor(context: Context) : this(context, null, 0) @@ -88,6 +94,9 @@ internal class PollView : RecyclerView { onClosePollClick = { onClosePollClick(poll) }, onViewPollResultsClick = { onViewPollResultsClick(poll) }, onShowAllOptionsClick = { onShowAllPollOptionClick() }, + onSuggestOptionClick = { onSuggestOptionClick(poll) }, + onAddCommentClick = { onAddCommentClick(poll) }, + onViewCommentsClick = { onViewCommentsClick(poll) }, ) adapter = pollAdapter } @@ -122,6 +131,16 @@ internal class PollView : RecyclerView { .takeIf { poll.options.size > PollsConstants.MAX_NUMBER_OF_VISIBLE_OPTIONS } ?.let(pollItems::add) + PollItem.SuggestOption.takeIf { poll.allowUserSuggestedOptions && !poll.closed } + ?.let(pollItems::add) + + PollItem.AddComment.takeIf { poll.allowAnswers && poll.answers.isEmpty() && !poll.closed } + ?.let(pollItems::add) + + PollItem.ViewComments(count = poll.answers.size) + .takeIf { poll.allowAnswers && poll.answers.isNotEmpty() } + ?.let(pollItems::add) + pollItems.add(PollItem.ViewResults) PollItem.Close.takeIf { isMine && !poll.closed } @@ -131,12 +150,16 @@ internal class PollView : RecyclerView { } } +@Suppress("LongParameterList") private class PollAdapter( private val pollViewStyle: PollViewStyle, private val onOptionClick: (Option) -> Unit, private val onClosePollClick: () -> Unit, private val onViewPollResultsClick: () -> Unit, private val onShowAllOptionsClick: () -> Unit, + private val onSuggestOptionClick: () -> Unit, + private val onAddCommentClick: () -> Unit, + private val onViewCommentsClick: () -> Unit, ) : ListAdapter>(PollItemDiffCallback) { companion object { @@ -145,6 +168,9 @@ private class PollAdapter( private const val VIEW_TYPE_CLOSE = 3 private const val VIEW_TYPE_RESULTS = 4 private const val VIEW_TYPE_SHOW_ALL_OPTIONS = 5 + private const val VIEW_TYPE_SUGGEST_OPTION = 6 + private const val VIEW_TYPE_ADD_COMMENT = 7 + private const val VIEW_TYPE_VIEW_COMMENTS = 8 } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollItemViewHolder { @@ -178,6 +204,24 @@ private class PollAdapter( onShowAllOptionsClick, ) + VIEW_TYPE_SUGGEST_OPTION -> SuggestOptionViewHolder( + StreamUiItemPollSuggestOptionBinding.inflate(parent.streamThemeInflater, parent, false) + .applyStyle(pollViewStyle), + onSuggestOptionClick, + ) + + VIEW_TYPE_ADD_COMMENT -> AddCommentViewHolder( + StreamUiItemPollAddCommentBinding.inflate(parent.streamThemeInflater, parent, false) + .applyStyle(pollViewStyle), + onAddCommentClick, + ) + + VIEW_TYPE_VIEW_COMMENTS -> ViewCommentsViewHolder( + StreamUiItemPollViewCommentsBinding.inflate(parent.streamThemeInflater, parent, false) + .applyStyle(pollViewStyle), + onViewCommentsClick, + ) + else -> throw IllegalArgumentException("Unknown view type: $viewType") } } @@ -188,6 +232,9 @@ private class PollAdapter( PollItem.Close -> VIEW_TYPE_CLOSE PollItem.ViewResults -> VIEW_TYPE_RESULTS is PollItem.ShowAllOptions -> VIEW_TYPE_SHOW_ALL_OPTIONS + PollItem.SuggestOption -> VIEW_TYPE_SUGGEST_OPTION + PollItem.AddComment -> VIEW_TYPE_ADD_COMMENT + is PollItem.ViewComments -> VIEW_TYPE_VIEW_COMMENTS } override fun onBindViewHolder(holder: PollItemViewHolder, position: Int) { @@ -225,6 +272,9 @@ private sealed class PollItem { data object Close : PollItem() data class ShowAllOptions(val count: Int) : PollItem() data object ViewResults : PollItem() + data object SuggestOption : PollItem() + data object AddComment : PollItem() + data class ViewComments(val count: Int) : PollItem() } private sealed class PollItemViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) { @@ -335,6 +385,38 @@ private class ViewResultsViewHolder( } } +private class SuggestOptionViewHolder( + private val binding: StreamUiItemPollSuggestOptionBinding, + private val onSuggestOptionClick: () -> Unit, +) : PollItemViewHolder(binding) { + override fun bind(pollItem: PollItem.SuggestOption) { + binding.root.setOnClickListener { onSuggestOptionClick() } + } +} + +private class AddCommentViewHolder( + private val binding: StreamUiItemPollAddCommentBinding, + private val onAddCommentClick: () -> Unit, +) : PollItemViewHolder(binding) { + override fun bind(pollItem: PollItem.AddComment) { + binding.root.setOnClickListener { onAddCommentClick() } + } +} + +private class ViewCommentsViewHolder( + private val binding: StreamUiItemPollViewCommentsBinding, + private val onViewCommentsClick: () -> Unit, +) : PollItemViewHolder(binding) { + override fun bind(pollItem: PollItem.ViewComments) { + binding.pollViewComments.text = binding.root.resources.getQuantityString( + UiCommonR.plurals.stream_ui_poll_action_view_comments, + pollItem.count, + pollItem.count, + ) + binding.root.setOnClickListener { onViewCommentsClick() } + } +} + private fun StreamUiItemPollHeaderBinding.applyStyle(style: PollViewStyle) = this.apply { title.setTextStyle(style.pollTitleTextStyle) subtitle.setTextStyle(style.pollSubtitleTextStyle) @@ -357,3 +439,15 @@ private fun StreamUiItemPollResultsBinding.applyStyle(style: PollViewStyle) = th private fun StreamUiItemPollShowAllOptionsBinding.applyStyle(style: PollViewStyle) = this.apply { pollShowAllOptions.setTextStyle(style.pollShowAllOptionsTextStyle) } + +private fun StreamUiItemPollSuggestOptionBinding.applyStyle(style: PollViewStyle) = this.apply { + pollSuggestOption.setTextStyle(style.pollSuggestOptionTextStyle) +} + +private fun StreamUiItemPollAddCommentBinding.applyStyle(style: PollViewStyle) = this.apply { + pollAddComment.setTextStyle(style.pollAddCommentTextStyle) +} + +private fun StreamUiItemPollViewCommentsBinding.applyStyle(style: PollViewStyle) = this.apply { + pollViewComments.setTextStyle(style.pollViewCommentsTextStyle) +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/PollViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/PollViewHolder.kt index 5bf576ca0a1..df0c3784d1f 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/PollViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/PollViewHolder.kt @@ -56,6 +56,15 @@ public class PollViewHolder( binding.pollView.onShowAllPollOptionClick = { messageListListeners?.onShowAllPollOptionClickListener?.onShowAllPollOptionClick(data.message, poll) } + binding.pollView.onSuggestOptionClick = { + messageListListeners?.onSuggestPollOptionClickListener?.onSuggestPollOptionClick(poll) + } + binding.pollView.onAddCommentClick = { + messageListListeners?.onAddPollCommentClickListener?.onAddPollCommentClick(data.message, poll) + } + binding.pollView.onViewCommentsClick = { + messageListListeners?.onViewPollCommentsClickListener?.onViewPollCommentsClick(data.message, poll) + } } } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/AddPollCommentDialogFragment.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/AddPollCommentDialogFragment.kt new file mode 100644 index 00000000000..e4e06b48587 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/AddPollCommentDialogFragment.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2014-2026 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.feature.messages.list.internal.poll + +import android.app.Dialog +import android.os.Bundle +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.core.os.bundleOf +import androidx.core.widget.doAfterTextChanged +import io.getstream.chat.android.ui.R +import io.getstream.chat.android.ui.databinding.StreamUiDialogAddPollCommentBinding +import io.getstream.chat.android.ui.feature.messages.list.internal.poll.AddPollCommentDialogFragment.Companion.BUNDLE_KEY_ANSWER +import io.getstream.chat.android.ui.feature.messages.list.internal.poll.AddPollCommentDialogFragment.Companion.BUNDLE_KEY_MESSAGE_ID +import io.getstream.chat.android.ui.feature.messages.list.internal.poll.AddPollCommentDialogFragment.Companion.BUNDLE_KEY_POLL_ID +import io.getstream.chat.android.ui.feature.messages.list.internal.poll.AddPollCommentDialogFragment.Companion.REQUEST_KEY +import io.getstream.chat.android.ui.utils.extensions.createStreamThemeWrapper +import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater + +/** + * Dialog that lets a voter add (or edit) a free-text comment / answer on a poll. + * + * Delivers the entered comment via the fragment-result API under [REQUEST_KEY], with + * the message id in [BUNDLE_KEY_MESSAGE_ID], the poll id in [BUNDLE_KEY_POLL_ID], + * and the trimmed answer in [BUNDLE_KEY_ANSWER]. + */ +internal class AddPollCommentDialogFragment : AppCompatDialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val args = requireArguments() + val messageId = args.getString(ARG_MESSAGE_ID) ?: error("Message ID not found in arguments") + val pollId = args.getString(ARG_POLL_ID) ?: error("Poll ID not found in arguments") + val initialText = args.getString(ARG_INITIAL_TEXT) + + val binding = StreamUiDialogAddPollCommentBinding.inflate(requireContext().streamThemeInflater) + if (savedInstanceState == null && !initialText.isNullOrEmpty()) { + binding.answerInput.setText(initialText) + binding.answerInput.setSelection(initialText.length) + } + + val titleRes = if (initialText.isNullOrEmpty()) { + R.string.stream_ui_poll_add_a_comment_label + } else { + R.string.stream_ui_poll_update_comment_label + } + + val dialog = AlertDialog.Builder(requireContext().createStreamThemeWrapper()) + .setTitle(titleRes) + .setView(binding.root) + .setPositiveButton(R.string.stream_ui_poll_add_comment_dialog_submit) { _, _ -> + val text = binding.answerInput.text?.toString()?.trim().orEmpty() + if (text.isNotEmpty()) { + parentFragmentManager.setFragmentResult( + REQUEST_KEY, + bundleOf( + BUNDLE_KEY_MESSAGE_ID to messageId, + BUNDLE_KEY_POLL_ID to pollId, + BUNDLE_KEY_ANSWER to text, + ), + ) + } + } + .setNegativeButton(R.string.stream_ui_poll_add_comment_dialog_cancel, null) + .create() + + dialog.setOnShowListener { + val confirm = dialog.getButton(AlertDialog.BUTTON_POSITIVE) + confirm.isEnabled = binding.answerInput.text?.toString()?.trim()?.isNotEmpty() == true + binding.answerInput.doAfterTextChanged { editable -> + confirm.isEnabled = editable?.toString()?.trim()?.isNotEmpty() == true + } + binding.answerInput.requestFocus() + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) + } + + return dialog + } + + companion object { + const val TAG: String = "AddPollCommentDialogFragment" + const val REQUEST_KEY: String = "stream_ui_add_poll_comment_request" + const val BUNDLE_KEY_MESSAGE_ID: String = "stream_ui_add_poll_comment_message_id" + const val BUNDLE_KEY_POLL_ID: String = "stream_ui_add_poll_comment_poll_id" + const val BUNDLE_KEY_ANSWER: String = "stream_ui_add_poll_comment_answer" + + private const val ARG_MESSAGE_ID: String = "arg_message_id" + private const val ARG_POLL_ID: String = "arg_poll_id" + private const val ARG_INITIAL_TEXT: String = "arg_initial_text" + + fun newInstance( + messageId: String, + pollId: String, + initialText: String? = null, + ): AddPollCommentDialogFragment = + AddPollCommentDialogFragment().apply { + arguments = bundleOf( + ARG_MESSAGE_ID to messageId, + ARG_POLL_ID to pollId, + ARG_INITIAL_TEXT to initialText, + ) + } + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/PollCommentsDialogFragment.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/PollCommentsDialogFragment.kt new file mode 100644 index 00000000000..a7d63202ece --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/PollCommentsDialogFragment.kt @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2014-2026 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.feature.messages.list.internal.poll + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.core.os.bundleOf +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import io.getstream.chat.android.models.Answer +import io.getstream.chat.android.models.Poll +import io.getstream.chat.android.models.VotingVisibility +import io.getstream.chat.android.ui.ChatUI +import io.getstream.chat.android.ui.R +import io.getstream.chat.android.ui.databinding.StreamUiFragmentPollCommentsBinding +import io.getstream.chat.android.ui.databinding.StreamUiItemPollCommentBinding +import io.getstream.chat.android.ui.utils.extensions.applyEdgeToEdgePadding +import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater +import io.getstream.chat.android.ui.viewmodel.messages.PollCommentsViewModel + +internal class PollCommentsDialogFragment : AppCompatDialogFragment() { + + private var _binding: StreamUiFragmentPollCommentsBinding? = null + private val binding get() = _binding!! + + private val cid: String + get() = requireArguments().getString(ARG_CID) ?: error("Channel cid not found in arguments") + + private val messageId: String + get() = requireArguments().getString(ARG_MESSAGE_ID) ?: error("Message ID not found in arguments") + + private val viewModel: PollCommentsViewModel by viewModels { + PollCommentsViewModel.Factory(cid = cid, messageId = messageId) + } + + private val commentsAdapter = CommentsAdapter() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = StreamUiFragmentPollCommentsBinding.inflate(requireContext().streamThemeInflater, container, false) + return binding.root + } + + override fun getTheme(): Int = R.style.StreamUiBottomSheetDialogTheme + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.root.applyEdgeToEdgePadding(typeMask = WindowInsetsCompat.Type.systemBars()) + binding.toolbar.setNavigationOnClickListener { dismiss() } + binding.commentList.adapter = commentsAdapter + viewModel.poll.observe(viewLifecycleOwner, ::render) + } + + private fun render(poll: Poll) { + val showUser = poll.votingVisibility == VotingVisibility.PUBLIC + commentsAdapter.submitList(poll.answers.map { CommentItem(it, showUser) }) + binding.ctaContainer.isVisible = !poll.closed + if (poll.closed) return + val ownAnswer = ChatUI.currentUserProvider.getCurrentUser()?.id + ?.let { id -> poll.answers.firstOrNull { it.user?.id == id } } + binding.ctaButton.setText( + if (ownAnswer == null) { + R.string.stream_ui_poll_add_a_comment_label + } else { + R.string.stream_ui_poll_update_comment_label + }, + ) + binding.ctaButton.setOnClickListener { + AddPollCommentDialogFragment + .newInstance(messageId = messageId, pollId = poll.id, initialText = ownAnswer?.text) + .show(parentFragmentManager, AddPollCommentDialogFragment.TAG) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + const val TAG: String = "PollCommentsDialogFragment" + private const val ARG_CID: String = "arg_cid" + private const val ARG_MESSAGE_ID: String = "arg_message_id" + + fun newInstance(cid: String, messageId: String): PollCommentsDialogFragment = + PollCommentsDialogFragment().apply { + arguments = bundleOf( + ARG_CID to cid, + ARG_MESSAGE_ID to messageId, + ) + } + } + + private data class CommentItem(val answer: Answer, val showUser: Boolean) + + private class CommentsAdapter : + ListAdapter(CommentDiffCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder = + CommentViewHolder( + StreamUiItemPollCommentBinding.inflate(LayoutInflater.from(parent.context), parent, false), + ) + + override fun onBindViewHolder(holder: CommentViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + private object CommentDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean = + oldItem.answer.id == newItem.answer.id + + override fun areContentsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean = + oldItem == newItem + } + + private class CommentViewHolder( + private val binding: StreamUiItemPollCommentBinding, + ) : RecyclerView.ViewHolder(binding.root) { + + fun bind(item: CommentItem) { + val answer = item.answer + binding.answerText.text = answer.text + val user = answer.user?.takeIf { item.showUser } + if (user != null) { + binding.name.text = user.name + binding.userAvatarView.setUser(user) + binding.userAvatarView.isVisible = true + } else { + binding.name.text = "" + binding.userAvatarView.isInvisible = true + } + binding.date.text = ChatUI.dateFormatter.formatRelativeDate(answer.createdAt) + binding.time.text = ChatUI.dateFormatter.formatTime(answer.createdAt) + } + } + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment.kt new file mode 100644 index 00000000000..ddc99afce86 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2014-2026 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.feature.messages.list.internal.poll + +import android.app.Dialog +import android.os.Bundle +import android.text.InputFilter +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.core.os.bundleOf +import androidx.core.widget.doAfterTextChanged +import io.getstream.chat.android.ui.ChatUI +import io.getstream.chat.android.ui.R +import io.getstream.chat.android.ui.databinding.StreamUiDialogSuggestPollOptionBinding +import io.getstream.chat.android.ui.utils.extensions.createStreamThemeWrapper +import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater + +/** + * Dialog that lets a voter suggest a new option for a poll. + * + * Delivers the entered option via the fragment-result API under [REQUEST_KEY], with + * the poll id in [BUNDLE_KEY_POLL_ID] and the option text in [BUNDLE_KEY_OPTION_TEXT]. + */ +internal class SuggestPollOptionDialogFragment : AppCompatDialogFragment() { + + private val pollId: String + get() = requireArguments().getString(ARG_POLL_ID) + ?: error("Poll ID not found in arguments") + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val binding = StreamUiDialogSuggestPollOptionBinding.inflate(requireContext().streamThemeInflater) + + ChatUI.pollsConfig.optionTextLimit?.let { limit -> + binding.optionInput.filters = arrayOf(InputFilter.LengthFilter(limit)) + } + + val dialog = AlertDialog.Builder(requireContext().createStreamThemeWrapper()) + .setTitle(R.string.stream_ui_poll_suggest_option_dialog_title) + .setView(binding.root) + .setPositiveButton(R.string.stream_ui_poll_suggest_option_dialog_submit) { _, _ -> + val text = binding.optionInput.text?.toString().orEmpty() + if (text.isNotBlank()) { + parentFragmentManager.setFragmentResult( + REQUEST_KEY, + bundleOf( + BUNDLE_KEY_POLL_ID to pollId, + BUNDLE_KEY_OPTION_TEXT to text, + ), + ) + } + } + .setNegativeButton(R.string.stream_ui_poll_suggest_option_dialog_cancel, null) + .create() + + dialog.setOnShowListener { + val confirm = dialog.getButton(AlertDialog.BUTTON_POSITIVE) + confirm.isEnabled = binding.optionInput.text?.toString()?.isNotBlank() == true + binding.optionInput.doAfterTextChanged { editable -> + confirm.isEnabled = editable?.toString()?.isNotBlank() == true + } + binding.optionInput.requestFocus() + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) + } + + return dialog + } + + companion object { + internal const val TAG: String = "SuggestPollOptionDialogFragment" + internal const val REQUEST_KEY: String = "stream_ui_suggest_poll_option_request" + internal const val BUNDLE_KEY_POLL_ID: String = "stream_ui_suggest_poll_option_poll_id" + internal const val BUNDLE_KEY_OPTION_TEXT: String = "stream_ui_suggest_poll_option_text" + + private const val ARG_POLL_ID: String = "arg_poll_id" + + fun newInstance(pollId: String): SuggestPollOptionDialogFragment = + SuggestPollOptionDialogFragment().apply { + arguments = bundleOf(ARG_POLL_ID to pollId) + } + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt index b19b77a5b24..6aea34159f1 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt @@ -299,6 +299,15 @@ public class MessageListViewModel( option = event.option, ) is Event.PollClosed -> messageListController.closePoll(event.poll) + is Event.PollOptionSuggested -> messageListController.addPollOption( + pollId = event.pollId, + option = event.option, + ) + is Event.PollAnswerCast -> messageListController.castAnswer( + messageId = event.messageId, + pollId = event.pollId, + answer = event.answer, + ) } } @@ -814,5 +823,26 @@ public class MessageListViewModel( * @param poll The poll to be closed. */ public data class PollClosed(val poll: Poll) : Event() + + /** + * When the user suggests a new option for a poll. + * + * @param pollId The id of the poll to add the option to. + * @param option The text of the option suggested by the user. + */ + public data class PollOptionSuggested(val pollId: String, val option: String) : Event() + + /** + * When the user casts a free-text answer / comment on a poll. + * + * @param messageId The id of the message that hosts the poll. + * @param pollId The id of the poll to cast the answer on. + * @param answer The text of the answer / comment. + */ + public data class PollAnswerCast( + val messageId: String, + val pollId: String, + val answer: String, + ) : Event() } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelBinding.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelBinding.kt index 3b5c382abdd..2b405e30b09 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelBinding.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelBinding.kt @@ -26,7 +26,10 @@ import io.getstream.chat.android.models.Poll import io.getstream.chat.android.ui.ChatUI import io.getstream.chat.android.ui.feature.gallery.toAttachment import io.getstream.chat.android.ui.feature.messages.list.MessageListView +import io.getstream.chat.android.ui.feature.messages.list.internal.poll.AddPollCommentDialogFragment +import io.getstream.chat.android.ui.feature.messages.list.internal.poll.SuggestPollOptionDialogFragment import io.getstream.chat.android.ui.utils.PermissionChecker +import io.getstream.chat.android.ui.utils.extensions.getFragmentManager import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel.Event.BottomEndRegionReached import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel.Event.DeleteMessage import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel.Event.DownloadAttachment @@ -186,4 +189,24 @@ public fun MessageListViewModel.bindView( view.setOnPollCloseClickListener { poll: Poll -> true.also { onEvent(MessageListViewModel.Event.PollClosed(poll)) } } + view.context.getFragmentManager() + ?.setFragmentResultListener(SuggestPollOptionDialogFragment.REQUEST_KEY, lifecycleOwner) { _, bundle -> + val pollId = + bundle.getString(SuggestPollOptionDialogFragment.BUNDLE_KEY_POLL_ID) ?: return@setFragmentResultListener + val optionText = bundle.getString(SuggestPollOptionDialogFragment.BUNDLE_KEY_OPTION_TEXT)?.trim() + ?.takeIf(String::isNotEmpty) + ?: return@setFragmentResultListener + onEvent(MessageListViewModel.Event.PollOptionSuggested(pollId, optionText)) + } + view.context.getFragmentManager() + ?.setFragmentResultListener(AddPollCommentDialogFragment.REQUEST_KEY, lifecycleOwner) { _, bundle -> + val messageId = bundle.getString(AddPollCommentDialogFragment.BUNDLE_KEY_MESSAGE_ID) + ?: return@setFragmentResultListener + val pollId = bundle.getString(AddPollCommentDialogFragment.BUNDLE_KEY_POLL_ID) + ?: return@setFragmentResultListener + val answer = bundle.getString(AddPollCommentDialogFragment.BUNDLE_KEY_ANSWER)?.trim() + ?.takeIf(String::isNotEmpty) + ?: return@setFragmentResultListener + onEvent(MessageListViewModel.Event.PollAnswerCast(messageId = messageId, pollId = pollId, answer = answer)) + } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/PollCommentsViewModel.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/PollCommentsViewModel.kt new file mode 100644 index 00000000000..51790c4c9a9 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/PollCommentsViewModel.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2014-2026 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.messages + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.asLiveData +import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.client.api.state.StateRegistry +import io.getstream.chat.android.client.api.state.state +import io.getstream.chat.android.client.extensions.cidToTypeAndId +import io.getstream.chat.android.models.Poll +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map + +/** + * Observes the live [Poll] hosted by the message identified by [messageId] inside the channel + * identified by [cid]. + */ +internal class PollCommentsViewModel( + cid: String, + messageId: String, + state: StateRegistry = ChatClient.instance().state, +) : ViewModel() { + + val poll: LiveData = run { + val (channelType, channelId) = cid.cidToTypeAndId() + state.channel(channelType, channelId).messages + .map { messages -> messages.find { it.id == messageId }?.poll } + .filterNotNull() + .distinctUntilChanged() + .asLiveData() + } + + class Factory( + private val cid: String, + private val messageId: String, + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + require(modelClass == PollCommentsViewModel::class.java) { + "Factory can only create instances of PollCommentsViewModel" + } + return PollCommentsViewModel(cid = cid, messageId = messageId) as T + } + } +} diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_dialog_add_poll_comment.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_dialog_add_poll_comment.xml new file mode 100644 index 00000000000..78de674c296 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_dialog_add_poll_comment.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_dialog_suggest_poll_option.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_dialog_suggest_poll_option.xml new file mode 100644 index 00000000000..d8c696e3a06 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_dialog_suggest_poll_option.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_fragment_poll_comments.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_fragment_poll_comments.xml new file mode 100644 index 00000000000..39d11eacc05 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_fragment_poll_comments.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_add_comment.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_add_comment.xml new file mode 100644 index 00000000000..0ef93dc498a --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_add_comment.xml @@ -0,0 +1,37 @@ + + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_comment.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_comment.xml new file mode 100644 index 00000000000..d89bcb57343 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_comment.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_suggest_option.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_suggest_option.xml new file mode 100644 index 00000000000..bb43e9599f9 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_suggest_option.xml @@ -0,0 +1,37 @@ + + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_view_comments.xml b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_view_comments.xml new file mode 100644 index 00000000000..0ee8d95ceb2 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_view_comments.xml @@ -0,0 +1,36 @@ + + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/values/attrs_poll_view.xml b/stream-chat-android-ui-components/src/main/res/values/attrs_poll_view.xml index 644b6382665..e00ed8007e7 100644 --- a/stream-chat-android-ui-components/src/main/res/values/attrs_poll_view.xml +++ b/stream-chat-android-ui-components/src/main/res/values/attrs_poll_view.xml @@ -125,5 +125,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stream-chat-android-ui-components/src/main/res/values/strings.xml b/stream-chat-android-ui-components/src/main/res/values/strings.xml index f725b78f349..2922a1f7b39 100644 --- a/stream-chat-android-ui-components/src/main/res/values/strings.xml +++ b/stream-chat-android-ui-components/src/main/res/values/strings.xml @@ -203,6 +203,13 @@ An error occurred while loading the poll results. Please try again later. Show All An error occurred while loading the poll option results. Please try again later. + Suggest an option + Confirm + Dismiss + Confirm + Dismiss + Poll Comments + Update comment Thread Reply Channel without name diff --git a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/CreatePollViewModelTest.kt b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/CreatePollViewModelTest.kt new file mode 100644 index 00000000000..f90c3af4d68 --- /dev/null +++ b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/CreatePollViewModelTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2014-2026 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.viewmodels.messages + +import app.cash.turbine.test +import io.getstream.chat.android.models.CreatePollParams +import io.getstream.chat.android.models.PollOption +import io.getstream.chat.android.models.VotingVisibility +import io.getstream.chat.android.test.TestCoroutineExtension +import io.getstream.chat.android.ui.feature.messages.composer.attachment.picker.poll.CreatePollViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@OptIn(ExperimentalCoroutinesApi::class) +internal class CreatePollViewModelTest { + + @Test + fun `Given poll options set When creating poll config Should emit matching CreatePollParams`() = runTest { + val viewModel = CreatePollViewModel() + viewModel.onTitleChanged("Favorite color?") + viewModel.createOption() + viewModel.onOptionTextChanged(id = 0, text = "Blue") + viewModel.createOption() + viewModel.onOptionTextChanged(id = 1, text = "Red") + viewModel.setAnnonymousPoll(true) + viewModel.setSuggestAnOption(true) + viewModel.setAllowAnswers(true) + viewModel.setAllowMultipleVotes(true) + viewModel.setMaxAnswer(3) + + viewModel.createPollParams.test { + awaitItem() shouldBeEqualTo null + + viewModel.createPollConfig() + + awaitItem() shouldBeEqualTo CreatePollParams( + name = "Favorite color?", + options = listOf(PollOption(text = "Blue"), PollOption(text = "Red")), + votingVisibility = VotingVisibility.ANONYMOUS, + enforceUniqueVote = false, + maxVotesAllowed = 3, + allowUserSuggestedOptions = true, + allowAnswers = true, + ) + } + } + + companion object { + @JvmField + @RegisterExtension + val testCoroutines = TestCoroutineExtension() + } +} diff --git a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageListViewModelTest.kt b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageListViewModelTest.kt index 553742e1763..6bbda944722 100644 --- a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageListViewModelTest.kt +++ b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageListViewModelTest.kt @@ -31,12 +31,15 @@ import io.getstream.chat.android.models.Config import io.getstream.chat.android.models.InitializationState import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.MessagesState +import io.getstream.chat.android.models.PollOption import io.getstream.chat.android.models.Reaction import io.getstream.chat.android.models.TypingEvent import io.getstream.chat.android.models.User import io.getstream.chat.android.randomChannelUserRead import io.getstream.chat.android.randomInt +import io.getstream.chat.android.randomPollOption import io.getstream.chat.android.randomString +import io.getstream.chat.android.randomVote import io.getstream.chat.android.test.InstantTaskExecutorExtension import io.getstream.chat.android.test.TestCoroutineExtension import io.getstream.chat.android.test.asCall @@ -189,6 +192,55 @@ internal class MessageListViewModelTest { ) } + @Test + fun `Given a poll When casting an answer Should cast poll answer`() = runTest { + val messages = listOf(message1, message2) + val chatClient = MockChatClientBuilder().build() + val pollId = randomString() + val answer = randomString() + + val viewModel = Fixture(chatClient = chatClient) + .givenCurrentUser() + .givenChannelQuery() + .givenChannelState(messageState = MessagesState.Result(messages), messages = messages) + .givenCastPollAnswer() + .get() + + viewModel.onEvent( + MessageListViewModel.Event.PollAnswerCast( + messageId = message1.id, + pollId = pollId, + answer = answer, + ), + ) + + verify(chatClient).castPollAnswer(messageId = message1.id, pollId = pollId, answer = answer) + } + + @Test + fun `Given a poll When suggesting an option Should create poll option`() = runTest { + val messages = listOf(message1, message2) + val chatClient = MockChatClientBuilder().build() + val pollId = randomString() + val option = randomString() + + val viewModel = Fixture(chatClient = chatClient) + .givenCurrentUser() + .givenChannelQuery() + .givenChannelState(messageState = MessagesState.Result(messages), messages = messages) + .givenCreatePollOption() + .get() + + viewModel.onEvent( + MessageListViewModel.Event.PollOptionSuggested( + pollId = pollId, + option = option, + ), + ) + + verify(chatClient).createPollOption(pollId, PollOption(text = option)) + } + @Test fun `Given no previous own reactions on a message When leaving a reaction Should leave reaction`() = runTest { val messages = listOf(message1, message2) @@ -330,6 +382,14 @@ internal class MessageListViewModelTest { whenever(chatClient.deleteReaction(any(), any(), any())) doReturn Message().asCall() } + fun givenCastPollAnswer() = apply { + whenever(chatClient.castPollAnswer(any(), any(), any())) doReturn randomVote().asCall() + } + + fun givenCreatePollOption() = apply { + whenever(chatClient.createPollOption(any(), any())) doReturn randomPollOption().asCall() + } + fun givenChannelState( channelData: ChannelData = ChannelData( type = CHANNEL_TYPE, @@ -381,7 +441,6 @@ internal class MessageListViewModelTest { threadLoadOrderOlderToNewer = false, channelState = MutableStateFlow(channelState), ), - ) } } diff --git a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/PollCommentsViewModelTest.kt b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/PollCommentsViewModelTest.kt new file mode 100644 index 00000000000..03b71b181e7 --- /dev/null +++ b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/PollCommentsViewModelTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2014-2026 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.viewmodels.messages + +import io.getstream.chat.android.client.api.state.StateRegistry +import io.getstream.chat.android.client.channel.state.ChannelState +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.randomMessage +import io.getstream.chat.android.randomPoll +import io.getstream.chat.android.test.InstantTaskExecutorExtension +import io.getstream.chat.android.test.TestCoroutineExtension +import io.getstream.chat.android.test.observeAll +import io.getstream.chat.android.ui.viewmodel.messages.PollCommentsViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEmpty +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +@OptIn(ExperimentalCoroutinesApi::class) +internal class PollCommentsViewModelTest { + + @Test + fun `Given a message hosting a poll When observing Should emit the poll`() = runTest { + val poll = randomPoll() + val targetMessage = randomMessage(id = TARGET_MESSAGE_ID, poll = poll) + val otherMessage = randomMessage(id = "other-message-id", poll = null) + + val viewModel = Fixture() + .givenChannelMessages(listOf(otherMessage, targetMessage)) + .get() + + val emissions = viewModel.poll.observeAll() + advanceUntilIdle() + + emissions shouldBeEqualTo listOf(poll) + } + + @Test + fun `Given the hosting message has no poll When observing Should not emit`() = runTest { + val targetMessage = randomMessage(id = TARGET_MESSAGE_ID, poll = null) + + val viewModel = Fixture() + .givenChannelMessages(listOf(targetMessage)) + .get() + + val emissions = viewModel.poll.observeAll() + advanceUntilIdle() + + emissions.shouldBeEmpty() + } + + @Test + fun `Given the poll on the hosting message changes When observing Should emit each distinct poll`() = runTest { + val firstPoll = randomPoll() + val secondPoll = randomPoll() + val fixture = Fixture() + .givenChannelMessages(listOf(randomMessage(id = TARGET_MESSAGE_ID, poll = firstPoll))) + val viewModel = fixture.get() + + val emissions = viewModel.poll.observeAll() + advanceUntilIdle() + + fixture.givenChannelMessages(listOf(randomMessage(id = TARGET_MESSAGE_ID, poll = firstPoll))) + advanceUntilIdle() + + fixture.givenChannelMessages(listOf(randomMessage(id = TARGET_MESSAGE_ID, poll = secondPoll))) + advanceUntilIdle() + + emissions shouldBeEqualTo listOf(firstPoll, secondPoll) + } + + private class Fixture { + private val messages = MutableStateFlow>(emptyList()) + private val channelState: ChannelState = mock { + on { messages } doReturn this@Fixture.messages + } + private val state: StateRegistry = mock { + on { channel(any(), any()) } doReturn channelState + } + + fun givenChannelMessages(messages: List) = apply { + this.messages.value = messages + } + + fun get(): PollCommentsViewModel = PollCommentsViewModel( + cid = CID, + messageId = TARGET_MESSAGE_ID, + state = state, + ) + } + + companion object { + @JvmField + @RegisterExtension + val testCoroutines = TestCoroutineExtension() + + @JvmField + @RegisterExtension + val instantExecutor = InstantTaskExecutorExtension() + + private const val CID = "messaging:123" + private const val TARGET_MESSAGE_ID = "target-message-id" + } +}