From 11cacffbdc54e0c0ff4c6f0e42d49319ac5fb5f5 Mon Sep 17 00:00:00 2001 From: Gianmarco <47775302+gpunto@users.noreply.github.com> Date: Mon, 18 May 2026 10:16:49 +0200 Subject: [PATCH 01/14] Fix MessageListView rotation crash and custom messagesStart not being applied (#6438) --- .../ui/feature/messages/list/MessageListView.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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..11c7c4f3a28 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 @@ -656,9 +656,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 +704,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 +1308,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 } } From 98192b0e6c120c7fa6b4d0d534cbba8e974e834e Mon Sep 17 00:00:00 2001 From: Ryan Hurst <106116154+ryanhurststrava@users.noreply.github.com> Date: Mon, 18 May 2026 02:39:14 -0600 Subject: [PATCH 02/14] Add configurable character limits and feature toggles for polls (#6435) * Add configurable character limits and feature toggles for polls Introduces PollsConfig to control poll feature availability and enforce character limits on questions and options. Poll features (multiple votes, anonymous voting, suggest options, add comments) can now be hidden or preset with default values through ChatUI.pollsConfig or passed directly to CreatePollDialogFragment. * addressing pr comments * 1. Detekt - Fixed the MaxLineLength violation in PollsConfig.kt:23 by breaking the long comment line 2. Spotless - Applied formatting fixes (added license header to PollFeatureConfig.kt) 3. API Check - Regenerated the API dump file * Update stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig.kt Co-authored-by: Gianmarco <47775302+gpunto@users.noreply.github.com> * Apply suggestion from @gpunto --------- Co-authored-by: Gianmarco <47775302+gpunto@users.noreply.github.com> --- .../api/stream-chat-android-ui-components.api | 79 +++++++++++++++++- .../io/getstream/chat/android/ui/ChatUI.kt | 10 +++ .../picker/poll/CreatePollDialogFragment.kt | 81 ++++++++++++++++++- .../picker/poll/CreatePollViewModel.kt | 11 +++ .../attachment/picker/poll/OptionsAdapter.kt | 17 ++++ .../picker/poll/PollFeatureConfig.kt | 50 ++++++++++++ .../attachment/picker/poll/PollsConfig.kt | 56 +++++++++++++ 7 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollFeatureConfig.kt create mode 100644 stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/PollsConfig.kt 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..b16aa6b8e6d 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 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..4aeacf39729 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,12 +25,14 @@ 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 @@ -41,6 +44,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 +53,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 +88,49 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() { super.onViewCreated(view, savedInstanceState) binding.root.applyEdgeToEdgePadding(typeMask = WindowInsetsCompat.Type.systemBars()) setupDialog() + + if (savedInstanceState == null) { + // Configure poll feature visibility and default values based on pollsConfig + configurePollFeatures() + } + } + + /** + * Configures the visibility and default values of poll features based on [pollsConfig]. + */ + private fun configurePollFeatures() { + // Configure multiple votes feature + createPollViewModel.setAllowMultipleVotes(pollsConfig.multipleVotes.defaultValue) + binding.multipleAnswersLabel.isVisible = pollsConfig.multipleVotes.configurable + binding.multipleAnswersSwitch.isVisible = pollsConfig.multipleVotes.configurable + if (pollsConfig.multipleVotes.configurable) { + binding.multipleAnswersSwitch.isChecked = pollsConfig.multipleVotes.defaultValue + binding.multipleAnswersCount.isVisible = pollsConfig.multipleVotes.defaultValue + } + + // Configure anonymous poll feature + createPollViewModel.setAnnonymousPoll(pollsConfig.anonymousPoll.defaultValue) + binding.anonymousPollLabel.isVisible = pollsConfig.anonymousPoll.configurable + binding.anonymousPollSwitch.isVisible = pollsConfig.anonymousPoll.configurable + if (pollsConfig.anonymousPoll.configurable) { + binding.anonymousPollSwitch.isChecked = pollsConfig.anonymousPoll.defaultValue + } + + // Configure suggest an option feature + createPollViewModel.setSuggestAnOption(pollsConfig.suggestAnOption.defaultValue) + binding.suggestAnOptionLabel.isVisible = pollsConfig.suggestAnOption.configurable + binding.suggestAnOptionSwitch.isVisible = pollsConfig.suggestAnOption.configurable + if (pollsConfig.suggestAnOption.configurable) { + binding.suggestAnOptionSwitch.isChecked = pollsConfig.suggestAnOption.defaultValue + } + + // Configure add a comment feature + createPollViewModel.setAllowAnswers(pollsConfig.allowComments.defaultValue) + binding.addACommentLabel.isVisible = pollsConfig.allowComments.configurable + binding.addACommentLabelSwitch.isVisible = pollsConfig.allowComments.configurable + if (pollsConfig.allowComments.configurable) { + binding.addACommentLabelSwitch.isChecked = pollsConfig.allowComments.defaultValue + } } /** @@ -82,6 +138,10 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() { */ private fun setupDialog() { setupToolbar(binding.toolbar) + pollsConfig.questionTextLimit?.takeIf { it > 0 }?.let { limit -> + binding.question.filters = arrayOf(InputFilter.LengthFilter(limit)) + } + binding.multipleAnswersSwitch.setOnCheckedChangeListener { _, isChecked -> binding.multipleAnswersCount.isVisible = isChecked createPollViewModel.setAllowMultipleVotes(isChecked) @@ -98,6 +158,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 +222,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..a9c0262a401 --- /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,56 @@ +/* + * 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." + } + } + + public companion object { + /** + * The default configuration for polls. All features are configurable and disabled by default. + */ + public val Default: PollsConfig = PollsConfig() + } +} From 682dca9b0ec9bd7755bc349290eba052118710ae Mon Sep 17 00:00:00 2001 From: Gianmarco <47775302+gpunto@users.noreply.github.com> Date: Tue, 19 May 2026 12:09:54 +0200 Subject: [PATCH 03/14] Allow voters to suggest poll options in UI Components (#6439) * Allow voters to suggest poll options in UI Components * Use doAfterTextChanged * Align Compose & XML option name trimming --- .../api/stream-chat-android-ui-common.api | 1 + .../messages/list/MessageListController.kt | 12 ++- .../list/MessageListControllerTests.kt | 21 +++++ .../api/stream-chat-android-ui-components.api | 37 +++++++- .../feature/messages/list/MessageListView.kt | 28 ++++++ .../list/adapter/MessageListListeners.kt | 2 + .../list/adapter/MessageListListenersImpl.kt | 10 +++ .../list/adapter/view/PollViewStyle.kt | 21 +++++ .../list/adapter/view/internal/PollView.kt | 29 ++++++ .../adapter/viewholder/impl/PollViewHolder.kt | 3 + .../poll/SuggestPollOptionDialogFragment.kt | 90 +++++++++++++++++++ .../messages/MessageListViewModel.kt | 12 +++ .../messages/MessageListViewModelBinding.kt | 11 +++ .../stream_ui_dialog_suggest_poll_option.xml | 32 +++++++ .../stream_ui_item_poll_suggest_option.xml | 37 ++++++++ .../src/main/res/values/attrs_poll_view.xml | 10 +++ .../src/main/res/values/strings.xml | 3 + 17 files changed, 355 insertions(+), 4 deletions(-) create mode 100644 stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment.kt create mode 100644 stream-chat-android-ui-components/src/main/res/layout/stream_ui_dialog_suggest_poll_option.xml create mode 100644 stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_suggest_option.xml 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 b16aa6b8e6d..4aeeec6e3d8 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 @@ -2324,6 +2324,7 @@ 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 @@ -2530,6 +2531,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 } @@ -3049,6 +3054,7 @@ public abstract interface class io/getstream/chat/android/ui/feature/messages/li 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 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; @@ -3129,7 +3135,7 @@ 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;)V public final fun component1 ()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; @@ -3138,8 +3144,9 @@ 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/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;ILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle; public fun equals (Ljava/lang/Object;)Z public final fun getPollCloseTextStyle ()Lio/getstream/chat/android/ui/font/TextStyle; public final fun getPollOptionCheckDrawable ()Landroid/graphics/drawable/Drawable; @@ -3148,6 +3155,7 @@ 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 fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -3425,6 +3433,16 @@ public final class io/getstream/chat/android/ui/feature/messages/list/internal/p public final fun newInstance (Lio/getstream/chat/android/models/Poll;)Lio/getstream/chat/android/ui/feature/messages/list/internal/poll/PollResultsDialogFragment; } +public final class io/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment : androidx/appcompat/app/AppCompatDialogFragment { + public static final field Companion Lio/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment$Companion; + public fun ()V + public fun onCreateDialog (Landroid/os/Bundle;)Landroid/app/Dialog; +} + +public final class io/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment$Companion { + public final fun newInstance (Ljava/lang/String;)Lio/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment; +} + public class io/getstream/chat/android/ui/feature/messages/list/options/message/DefaultMessageOptionItemsFactory : io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory { public fun (Landroid/content/Context;)V public fun createMessageOptionItems (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;ZLjava/util/Set;Lio/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle;)Ljava/util/List; @@ -4897,6 +4915,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/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 11c7c4f3a28..52064ac5024 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 @@ -95,6 +95,7 @@ import io.getstream.chat.android.ui.feature.messages.list.internal.SwipeReplyCal import io.getstream.chat.android.ui.feature.messages.list.internal.canReplyToMessage import io.getstream.chat.android.ui.feature.messages.list.internal.poll.AllPollOptionsDialogFragment 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 +610,13 @@ 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 listenerContainer = MessageListListenersImpl( messageClickListener = defaultMessageClickListener, @@ -626,6 +634,7 @@ public class MessageListView : ConstraintLayout { onShowAllPollOptionClickListener = defaultOnShowAllPollOptionClickListener, onPollCloseClickListener = defaultOnPollCloseClickListener, onViewPollResultClickListener = defaultOnViewPollResultClickListener, + onSuggestPollOptionClickListener = defaultOnSuggestPollOptionClickListener, ) private var enterThreadListener = defaultEnterThreadListener private var userReactionClickListener = defaultUserReactionClickListener @@ -1392,6 +1401,20 @@ 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) + } + } + /** * Sets the message long click listener to be used by MessageListView. * @@ -2010,6 +2033,11 @@ public class MessageListView : ConstraintLayout { public fun onViewPollResultClick(poll: Poll): Boolean } + public fun interface OnSuggestPollOptionClickListener { + public fun onSuggestPollOptionClick(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..81611a5ffb9 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 @@ -28,6 +28,7 @@ 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 @@ -52,4 +53,5 @@ public sealed interface MessageListListeners { public val onShowAllPollOptionClickListener: OnShowAllPollOptionClickListener public val onPollCloseClickListener: OnPollCloseClickListener public val onViewPollResultClickListener: OnViewPollResultClickListener + public val onSuggestPollOptionClickListener: OnSuggestPollOptionClickListener } 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..89e4b97ae8f 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 @@ -28,6 +28,7 @@ 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 @@ -57,6 +58,7 @@ internal class MessageListListenersImpl( onShowAllPollOptionClickListener: OnShowAllPollOptionClickListener, onPollCloseClickListener: OnPollCloseClickListener, onViewPollResultClickListener: OnViewPollResultClickListener, + onSuggestPollOptionClickListener: OnSuggestPollOptionClickListener, ) : MessageListListeners { private object EmptyFunctions { val ONE_PARAM: (Any) -> Boolean = { _ -> false } @@ -198,4 +200,12 @@ internal class MessageListListenersImpl( realListener().onViewPollResultClick(poll) } } + + override var onSuggestPollOptionClickListener: OnSuggestPollOptionClickListener by ListenerDelegate( + onSuggestPollOptionClickListener, + ) { realListener -> + OnSuggestPollOptionClickListener { poll -> + realListener().onSuggestPollOptionClick(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..b5156ec0404 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,7 @@ public data class PollViewStyle( public val pollCloseTextStyle: TextStyle, public val pollResultsTextStyle: TextStyle, public val pollShowAllOptionsTextStyle: TextStyle, + public val pollSuggestOptionTextStyle: TextStyle, ) : ViewStyle { internal companion object { @@ -184,6 +185,25 @@ 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() return PollViewStyle( pollTitleTextStyle = pollTitleTextStyle, pollSubtitleTextStyle = pollSubtitleTextStyle, @@ -193,6 +213,7 @@ public data class PollViewStyle( pollCloseTextStyle = pollCloseTextStyle, pollResultsTextStyle = pollResultsTextStyle, pollShowAllOptionsTextStyle = pollShowAllOptionsTextStyle, + pollSuggestOptionTextStyle = pollSuggestOptionTextStyle, ).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..162c89b91a1 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 @@ -39,6 +39,7 @@ 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.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 +58,7 @@ internal class PollView : RecyclerView { var onClosePollClick: ((Poll) -> Unit) = { _ -> } var onViewPollResultsClick: ((Poll) -> Unit) = { _ -> } var onShowAllPollOptionClick: (() -> Unit) = { } + var onSuggestOptionClick: ((Poll) -> Unit) = { _ -> } constructor(context: Context) : this(context, null, 0) @@ -88,6 +90,7 @@ internal class PollView : RecyclerView { onClosePollClick = { onClosePollClick(poll) }, onViewPollResultsClick = { onViewPollResultsClick(poll) }, onShowAllOptionsClick = { onShowAllPollOptionClick() }, + onSuggestOptionClick = { onSuggestOptionClick(poll) }, ) adapter = pollAdapter } @@ -122,6 +125,9 @@ 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) + pollItems.add(PollItem.ViewResults) PollItem.Close.takeIf { isMine && !poll.closed } @@ -137,6 +143,7 @@ private class PollAdapter( private val onClosePollClick: () -> Unit, private val onViewPollResultsClick: () -> Unit, private val onShowAllOptionsClick: () -> Unit, + private val onSuggestOptionClick: () -> Unit, ) : ListAdapter>(PollItemDiffCallback) { companion object { @@ -145,6 +152,7 @@ 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 } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollItemViewHolder { @@ -178,6 +186,12 @@ private class PollAdapter( onShowAllOptionsClick, ) + VIEW_TYPE_SUGGEST_OPTION -> SuggestOptionViewHolder( + StreamUiItemPollSuggestOptionBinding.inflate(parent.streamThemeInflater, parent, false) + .applyStyle(pollViewStyle), + onSuggestOptionClick, + ) + else -> throw IllegalArgumentException("Unknown view type: $viewType") } } @@ -188,6 +202,7 @@ 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 } override fun onBindViewHolder(holder: PollItemViewHolder, position: Int) { @@ -225,6 +240,7 @@ private sealed class PollItem { data object Close : PollItem() data class ShowAllOptions(val count: Int) : PollItem() data object ViewResults : PollItem() + data object SuggestOption : PollItem() } private sealed class PollItemViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) { @@ -335,6 +351,15 @@ 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 fun StreamUiItemPollHeaderBinding.applyStyle(style: PollViewStyle) = this.apply { title.setTextStyle(style.pollTitleTextStyle) subtitle.setTextStyle(style.pollSubtitleTextStyle) @@ -357,3 +382,7 @@ 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) +} 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..eb318ca1912 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,9 @@ public class PollViewHolder( binding.pollView.onShowAllPollOptionClick = { messageListListeners?.onShowAllPollOptionClickListener?.onShowAllPollOptionClick(data.message, poll) } + binding.pollView.onSuggestOptionClick = { + messageListListeners?.onSuggestPollOptionClickListener?.onSuggestPollOptionClick(poll) + } } } } 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..ad890344ea2 --- /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,90 @@ +/* + * 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.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]. + */ +public 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) + + 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 = false + binding.optionInput.doAfterTextChanged { editable -> + confirm.isEnabled = editable?.toString()?.trim()?.isNotEmpty() == true + } + binding.optionInput.requestFocus() + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) + } + + return dialog + } + + public 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" + + public 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..6c991de67bc 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,10 @@ public class MessageListViewModel( option = event.option, ) is Event.PollClosed -> messageListController.closePoll(event.poll) + is Event.PollOptionSuggested -> messageListController.addPollOption( + pollId = event.pollId, + option = event.option, + ) } } @@ -814,5 +818,13 @@ 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() } } 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..abcf4639706 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,9 @@ 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.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 +188,13 @@ 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)) + } } 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_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/values/attrs_poll_view.xml b/stream-chat-android-ui-components/src/main/res/values/attrs_poll_view.xml index 644b6382665..9f6d2d8a6d2 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,15 @@ + + + + + + + + + + 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..016f493be67 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,9 @@ 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 Thread Reply Channel without name From 934dff4296c0cf2a39bbdc7bb293b7b117fa46a7 Mon Sep 17 00:00:00 2001 From: Gianmarco <47775302+gpunto@users.noreply.github.com> Date: Wed, 20 May 2026 15:05:40 +0200 Subject: [PATCH 04/14] Apply optionTextLimit to suggest poll option dialog (#6446) --- .../list/internal/poll/SuggestPollOptionDialogFragment.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 index ad890344ea2..42f48a221f2 100644 --- 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 @@ -18,11 +18,13 @@ 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 @@ -43,6 +45,10 @@ public class SuggestPollOptionDialogFragment : AppCompatDialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val binding = StreamUiDialogSuggestPollOptionBinding.inflate(requireContext().streamThemeInflater) + ChatUI.pollsConfig.optionTextLimit?.takeIf { it > 0 }?.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) From e78591056f5fcd6fc3f7df43211c16f605d6e4a2 Mon Sep 17 00:00:00 2001 From: Gianmarco <47775302+gpunto@users.noreply.github.com> Date: Fri, 22 May 2026 16:37:15 +0200 Subject: [PATCH 05/14] Allow voters to add poll comments in UI Components (#6444) * Allow voters to add poll comments in UI Components * Add missing KDoc and default params --- .../io/getstream/chat/android/Mother.kt | 16 ++ .../api/stream-chat-android-ui-components.api | 38 +++- .../feature/messages/list/MessageListView.kt | 63 +++++++ .../list/adapter/MessageListListeners.kt | 4 + .../list/adapter/MessageListListenersImpl.kt | 21 +++ .../list/adapter/view/PollViewStyle.kt | 42 +++++ .../list/adapter/view/internal/PollView.kt | 65 +++++++ .../adapter/viewholder/impl/PollViewHolder.kt | 6 + .../poll/AddPollCommentDialogFragment.kt | 118 +++++++++++++ .../poll/PollCommentsDialogFragment.kt | 162 ++++++++++++++++++ .../messages/MessageListViewModel.kt | 18 ++ .../messages/MessageListViewModelBinding.kt | 12 ++ .../messages/PollCommentsViewModel.kt | 63 +++++++ .../stream_ui_dialog_add_poll_comment.xml | 32 ++++ .../stream_ui_fragment_poll_comments.xml | 75 ++++++++ .../stream_ui_item_poll_add_comment.xml | 37 ++++ .../layout/stream_ui_item_poll_comment.xml | 94 ++++++++++ .../stream_ui_item_poll_view_comments.xml | 36 ++++ .../src/main/res/values/attrs_poll_view.xml | 20 +++ .../src/main/res/values/strings.xml | 4 + .../messages/MessageListViewModelTest.kt | 31 +++- .../messages/PollCommentsViewModelTest.kt | 125 ++++++++++++++ 22 files changed, 1078 insertions(+), 4 deletions(-) create mode 100644 stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/AddPollCommentDialogFragment.kt create mode 100644 stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/poll/PollCommentsDialogFragment.kt create mode 100644 stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/PollCommentsViewModel.kt create mode 100644 stream-chat-android-ui-components/src/main/res/layout/stream_ui_dialog_add_poll_comment.xml create mode 100644 stream-chat-android-ui-components/src/main/res/layout/stream_ui_fragment_poll_comments.xml create mode 100644 stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_add_comment.xml create mode 100644 stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_comment.xml create mode 100644 stream-chat-android-ui-components/src/main/res/layout/stream_ui_item_poll_view_comments.xml create mode 100644 stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/PollCommentsViewModelTest.kt 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-components/api/stream-chat-android-ui-components.api b/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api index 4aeeec6e3d8..08c159b6a03 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 @@ -2309,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 @@ -2330,6 +2331,7 @@ public final class io/getstream/chat/android/ui/feature/messages/list/MessageLis 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 @@ -2463,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 } @@ -2559,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 } @@ -3051,10 +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; @@ -3135,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;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; @@ -3145,9 +3160,10 @@ public final class io/getstream/chat/android/ui/feature/messages/list/adapter/vi 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 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/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;ILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/messages/list/adapter/view/PollViewStyle; + 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; @@ -3157,6 +3173,7 @@ public final class io/getstream/chat/android/ui/feature/messages/list/adapter/vi 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; } @@ -4904,6 +4921,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; 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 52064ac5024..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,7 +93,9 @@ 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 @@ -617,6 +619,21 @@ public class MessageListView : ConstraintLayout { 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, @@ -635,6 +652,8 @@ public class MessageListView : ConstraintLayout { onPollCloseClickListener = defaultOnPollCloseClickListener, onViewPollResultClickListener = defaultOnViewPollResultClickListener, onSuggestPollOptionClickListener = defaultOnSuggestPollOptionClickListener, + onAddPollCommentClickListener = defaultOnAddPollCommentClickListener, + onViewPollCommentsClickListener = defaultOnViewPollCommentsClickListener, ) private var enterThreadListener = defaultEnterThreadListener private var userReactionClickListener = defaultUserReactionClickListener @@ -1415,6 +1434,34 @@ public class MessageListView : ConstraintLayout { } } + /** + * 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. * @@ -2033,10 +2080,26 @@ 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 81611a5ffb9..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 @@ -33,6 +34,7 @@ import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnThre 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 { @@ -54,4 +56,6 @@ public sealed interface MessageListListeners { 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 89e4b97ae8f..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 @@ -33,9 +34,11 @@ import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnThre 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), @@ -59,6 +62,8 @@ internal class MessageListListenersImpl( onPollCloseClickListener: OnPollCloseClickListener, onViewPollResultClickListener: OnViewPollResultClickListener, onSuggestPollOptionClickListener: OnSuggestPollOptionClickListener, + onAddPollCommentClickListener: OnAddPollCommentClickListener, + onViewPollCommentsClickListener: OnViewPollCommentsClickListener, ) : MessageListListeners { private object EmptyFunctions { val ONE_PARAM: (Any) -> Boolean = { _ -> false } @@ -208,4 +213,20 @@ internal class MessageListListenersImpl( 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 b5156ec0404..e367f18794a 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 @@ -39,6 +39,8 @@ public data class PollViewStyle( public val pollResultsTextStyle: TextStyle, public val pollShowAllOptionsTextStyle: TextStyle, public val pollSuggestOptionTextStyle: TextStyle, + public val pollAddCommentTextStyle: TextStyle = TextStyle(), + public val pollViewCommentsTextStyle: TextStyle = TextStyle(), ) : ViewStyle { internal companion object { @@ -204,6 +206,44 @@ public data class PollViewStyle( 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, @@ -214,6 +254,8 @@ public data class PollViewStyle( 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 162c89b91a1..1447248c5d3 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,12 +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 @@ -59,6 +61,8 @@ internal class PollView : RecyclerView { 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) @@ -91,6 +95,8 @@ internal class PollView : RecyclerView { onViewPollResultsClick = { onViewPollResultsClick(poll) }, onShowAllOptionsClick = { onShowAllPollOptionClick() }, onSuggestOptionClick = { onSuggestOptionClick(poll) }, + onAddCommentClick = { onAddCommentClick(poll) }, + onViewCommentsClick = { onViewCommentsClick(poll) }, ) adapter = pollAdapter } @@ -128,6 +134,13 @@ internal class PollView : RecyclerView { 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 } @@ -137,6 +150,7 @@ internal class PollView : RecyclerView { } } +@Suppress("LongParameterList") private class PollAdapter( private val pollViewStyle: PollViewStyle, private val onOptionClick: (Option) -> Unit, @@ -144,6 +158,8 @@ private class PollAdapter( private val onViewPollResultsClick: () -> Unit, private val onShowAllOptionsClick: () -> Unit, private val onSuggestOptionClick: () -> Unit, + private val onAddCommentClick: () -> Unit, + private val onViewCommentsClick: () -> Unit, ) : ListAdapter>(PollItemDiffCallback) { companion object { @@ -153,6 +169,8 @@ private class PollAdapter( 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 { @@ -192,6 +210,18 @@ private class PollAdapter( 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") } } @@ -203,6 +233,8 @@ private class PollAdapter( 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) { @@ -241,6 +273,8 @@ private sealed class 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) { @@ -360,6 +394,29 @@ private class SuggestOptionViewHolder( } } +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( + R.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) @@ -386,3 +443,11 @@ private fun StreamUiItemPollShowAllOptionsBinding.applyStyle(style: PollViewStyl 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 eb318ca1912..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 @@ -59,6 +59,12 @@ public class PollViewHolder( 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/viewmodel/messages/MessageListViewModel.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt index 6c991de67bc..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 @@ -303,6 +303,11 @@ public class MessageListViewModel( pollId = event.pollId, option = event.option, ) + is Event.PollAnswerCast -> messageListController.castAnswer( + messageId = event.messageId, + pollId = event.pollId, + answer = event.answer, + ) } } @@ -826,5 +831,18 @@ public class MessageListViewModel( * @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 abcf4639706..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,6 +26,7 @@ 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 @@ -197,4 +198,15 @@ public fun MessageListViewModel.bindView( ?: 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..627d49aad9a --- /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.extensions.cidToTypeAndId +import io.getstream.chat.android.models.Poll +import io.getstream.chat.android.state.extensions.state +import io.getstream.chat.android.state.plugin.state.StateRegistry +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_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_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 9f6d2d8a6d2..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 @@ -135,5 +135,25 @@ + + + + + + + + + + + + + + + + + + + + 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 016f493be67..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 @@ -206,6 +206,10 @@ 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/MessageListViewModelTest.kt b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/MessageListViewModelTest.kt index 553742e1763..3c23611b72d 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 @@ -37,6 +37,7 @@ import io.getstream.chat.android.models.User import io.getstream.chat.android.randomChannelUserRead import io.getstream.chat.android.randomInt 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 +190,31 @@ 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 no previous own reactions on a message When leaving a reaction Should leave reaction`() = runTest { val messages = listOf(message1, message2) @@ -330,6 +356,10 @@ 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 givenChannelState( channelData: ChannelData = ChannelData( type = CHANNEL_TYPE, @@ -381,7 +411,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..23fd13d91e5 --- /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.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.state.plugin.state.StateRegistry +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" + } +} From d73106c16a66e8cdabc24ca15827f5be1a6ed2b7 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Mon, 25 May 2026 09:59:55 +0200 Subject: [PATCH 06/14] Adjust poll comments commit to develop's APIs - Use UiCommonR for stream_ui_poll_action_view_comments plural - Update StateRegistry imports to client.api.state package --- .../feature/messages/list/adapter/view/internal/PollView.kt | 2 +- .../android/ui/viewmodel/messages/PollCommentsViewModel.kt | 4 ++-- .../ui/viewmodels/messages/PollCommentsViewModelTest.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 1447248c5d3..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 @@ -409,7 +409,7 @@ private class ViewCommentsViewHolder( ) : PollItemViewHolder(binding) { override fun bind(pollItem: PollItem.ViewComments) { binding.pollViewComments.text = binding.root.resources.getQuantityString( - R.plurals.stream_ui_poll_action_view_comments, + UiCommonR.plurals.stream_ui_poll_action_view_comments, pollItem.count, pollItem.count, ) 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 index 627d49aad9a..6de42557a7b 100644 --- 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 @@ -23,8 +23,8 @@ import androidx.lifecycle.asLiveData import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.extensions.cidToTypeAndId import io.getstream.chat.android.models.Poll -import io.getstream.chat.android.state.extensions.state -import io.getstream.chat.android.state.plugin.state.StateRegistry +import io.getstream.chat.android.client.api.state.StateRegistry +import io.getstream.chat.android.client.api.state.state import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map 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 index 23fd13d91e5..49225255216 100644 --- 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 @@ -20,7 +20,7 @@ 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.state.plugin.state.StateRegistry +import io.getstream.chat.android.client.api.state.StateRegistry import io.getstream.chat.android.test.InstantTaskExecutorExtension import io.getstream.chat.android.test.TestCoroutineExtension import io.getstream.chat.android.test.observeAll From 0db663866cf40896d3f0cb7886c4bde7ac1268e2 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Mon, 25 May 2026 10:26:47 +0200 Subject: [PATCH 07/14] Reformat --- .../android/ui/viewmodel/messages/PollCommentsViewModel.kt | 4 ++-- .../ui/viewmodels/messages/PollCommentsViewModelTest.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index 6de42557a7b..51790c4c9a9 100644 --- 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 @@ -21,10 +21,10 @@ 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.extensions.cidToTypeAndId -import io.getstream.chat.android.models.Poll 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 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 index 49225255216..03b71b181e7 100644 --- 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 @@ -16,11 +16,11 @@ 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.client.api.state.StateRegistry import io.getstream.chat.android.test.InstantTaskExecutorExtension import io.getstream.chat.android.test.TestCoroutineExtension import io.getstream.chat.android.test.observeAll From 07f967e83579fd29ad6a104bac592b1836e49953 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Mon, 25 May 2026 18:17:09 +0200 Subject: [PATCH 08/14] Add default value to pollSuggestOptionTextStyle --- .../ui/feature/messages/list/adapter/view/PollViewStyle.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e367f18794a..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,7 +38,7 @@ public data class PollViewStyle( public val pollCloseTextStyle: TextStyle, public val pollResultsTextStyle: TextStyle, public val pollShowAllOptionsTextStyle: TextStyle, - public val pollSuggestOptionTextStyle: TextStyle, + public val pollSuggestOptionTextStyle: TextStyle = TextStyle(), public val pollAddCommentTextStyle: TextStyle = TextStyle(), public val pollViewCommentsTextStyle: TextStyle = TextStyle(), ) : ViewStyle { From 6507d0803d437f7508d51b7e391c2b54ad55389f Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Mon, 25 May 2026 18:17:26 +0200 Subject: [PATCH 09/14] Init poll suggest option confirm button state from current input --- .../list/internal/poll/SuggestPollOptionDialogFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 42f48a221f2..055a3d13353 100644 --- 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 @@ -69,9 +69,9 @@ public class SuggestPollOptionDialogFragment : AppCompatDialogFragment() { dialog.setOnShowListener { val confirm = dialog.getButton(AlertDialog.BUTTON_POSITIVE) - confirm.isEnabled = false + confirm.isEnabled = binding.optionInput.text?.toString()?.isNotBlank() == true binding.optionInput.doAfterTextChanged { editable -> - confirm.isEnabled = editable?.toString()?.trim()?.isNotEmpty() == true + confirm.isEnabled = editable?.toString()?.isNotBlank() == true } binding.optionInput.requestFocus() dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) From 16c8d9370d89306c65e6c6d6456d9bcceb88dd1c Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Mon, 25 May 2026 18:17:51 +0200 Subject: [PATCH 10/14] Validate poll text limits at PollsConfig construction --- .../attachment/picker/poll/CreatePollDialogFragment.kt | 2 +- .../messages/composer/attachment/picker/poll/PollsConfig.kt | 6 ++++++ .../list/internal/poll/SuggestPollOptionDialogFragment.kt | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) 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 4aeacf39729..2fed58f2d92 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 @@ -138,7 +138,7 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() { */ private fun setupDialog() { setupToolbar(binding.toolbar) - pollsConfig.questionTextLimit?.takeIf { it > 0 }?.let { limit -> + pollsConfig.questionTextLimit?.let { limit -> binding.question.filters = arrayOf(InputFilter.LengthFilter(limit)) } 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 index a9c0262a401..a9d9bda5c1d 100644 --- 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 @@ -45,6 +45,12 @@ public data class PollsConfig( "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 { 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 index 055a3d13353..b1670e6bffc 100644 --- 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 @@ -45,7 +45,7 @@ public class SuggestPollOptionDialogFragment : AppCompatDialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val binding = StreamUiDialogSuggestPollOptionBinding.inflate(requireContext().streamThemeInflater) - ChatUI.pollsConfig.optionTextLimit?.takeIf { it > 0 }?.let { limit -> + ChatUI.pollsConfig.optionTextLimit?.let { limit -> binding.optionInput.filters = arrayOf(InputFilter.LengthFilter(limit)) } From 76318ccd76b153bdd2807bddd6c53d7c0eedc080 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Wed, 27 May 2026 11:26:14 +0200 Subject: [PATCH 11/14] Make SuggestPollOptionDialogFragment internal for consistency --- .../api/stream-chat-android-ui-components.api | 10 ---------- .../internal/poll/SuggestPollOptionDialogFragment.kt | 6 +++--- 2 files changed, 3 insertions(+), 13 deletions(-) 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 08c159b6a03..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 @@ -3450,16 +3450,6 @@ public final class io/getstream/chat/android/ui/feature/messages/list/internal/p public final fun newInstance (Lio/getstream/chat/android/models/Poll;)Lio/getstream/chat/android/ui/feature/messages/list/internal/poll/PollResultsDialogFragment; } -public final class io/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment : androidx/appcompat/app/AppCompatDialogFragment { - public static final field Companion Lio/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment$Companion; - public fun ()V - public fun onCreateDialog (Landroid/os/Bundle;)Landroid/app/Dialog; -} - -public final class io/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment$Companion { - public final fun newInstance (Ljava/lang/String;)Lio/getstream/chat/android/ui/feature/messages/list/internal/poll/SuggestPollOptionDialogFragment; -} - public class io/getstream/chat/android/ui/feature/messages/list/options/message/DefaultMessageOptionItemsFactory : io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory { public fun (Landroid/content/Context;)V public fun createMessageOptionItems (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/User;ZLjava/util/Set;Lio/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle;)Ljava/util/List; 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 index b1670e6bffc..ddc99afce86 100644 --- 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 @@ -36,7 +36,7 @@ import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater * 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]. */ -public class SuggestPollOptionDialogFragment : AppCompatDialogFragment() { +internal class SuggestPollOptionDialogFragment : AppCompatDialogFragment() { private val pollId: String get() = requireArguments().getString(ARG_POLL_ID) @@ -80,7 +80,7 @@ public class SuggestPollOptionDialogFragment : AppCompatDialogFragment() { return dialog } - public companion object { + 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" @@ -88,7 +88,7 @@ public class SuggestPollOptionDialogFragment : AppCompatDialogFragment() { private const val ARG_POLL_ID: String = "arg_poll_id" - public fun newInstance(pollId: String): SuggestPollOptionDialogFragment = + fun newInstance(pollId: String): SuggestPollOptionDialogFragment = SuggestPollOptionDialogFragment().apply { arguments = bundleOf(ARG_POLL_ID to pollId) } From fbdc21699344d12985681ed1e015fed8a17610b4 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Wed, 27 May 2026 11:36:46 +0200 Subject: [PATCH 12/14] Reapply poll feature visibility on rotation in CreatePollDialogFragment --- .../picker/poll/CreatePollDialogFragment.kt | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) 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 2fed58f2d92..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 @@ -36,6 +36,7 @@ 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 @@ -90,49 +91,52 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() { setupDialog() if (savedInstanceState == null) { - // Configure poll feature visibility and default values based on pollsConfig - configurePollFeatures() + applyFeatureDefaults() } + applyFeatureVisibility() } /** - * Configures the visibility and default values of poll features based on [pollsConfig]. + * Applies the default state of poll features to the ViewModel and switches based on [pollsConfig]. */ - private fun configurePollFeatures() { - // Configure multiple votes feature + private fun applyFeatureDefaults() { createPollViewModel.setAllowMultipleVotes(pollsConfig.multipleVotes.defaultValue) - binding.multipleAnswersLabel.isVisible = pollsConfig.multipleVotes.configurable - binding.multipleAnswersSwitch.isVisible = pollsConfig.multipleVotes.configurable if (pollsConfig.multipleVotes.configurable) { binding.multipleAnswersSwitch.isChecked = pollsConfig.multipleVotes.defaultValue - binding.multipleAnswersCount.isVisible = pollsConfig.multipleVotes.defaultValue } - // Configure anonymous poll feature createPollViewModel.setAnnonymousPoll(pollsConfig.anonymousPoll.defaultValue) - binding.anonymousPollLabel.isVisible = pollsConfig.anonymousPoll.configurable - binding.anonymousPollSwitch.isVisible = pollsConfig.anonymousPoll.configurable if (pollsConfig.anonymousPoll.configurable) { binding.anonymousPollSwitch.isChecked = pollsConfig.anonymousPoll.defaultValue } - // Configure suggest an option feature createPollViewModel.setSuggestAnOption(pollsConfig.suggestAnOption.defaultValue) - binding.suggestAnOptionLabel.isVisible = pollsConfig.suggestAnOption.configurable - binding.suggestAnOptionSwitch.isVisible = pollsConfig.suggestAnOption.configurable if (pollsConfig.suggestAnOption.configurable) { binding.suggestAnOptionSwitch.isChecked = pollsConfig.suggestAnOption.defaultValue } - // Configure add a comment feature createPollViewModel.setAllowAnswers(pollsConfig.allowComments.defaultValue) - binding.addACommentLabel.isVisible = pollsConfig.allowComments.configurable - binding.addACommentLabelSwitch.isVisible = pollsConfig.allowComments.configurable 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 + } + /** * Initializes the dialog. */ From 3818bbf6a8dab62366192381dd9654531317ce48 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Wed, 27 May 2026 11:41:13 +0200 Subject: [PATCH 13/14] Add MessageListViewModel test for suggesting a poll option --- .../messages/MessageListViewModelTest.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) 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 3c23611b72d..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,11 +31,13 @@ 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 @@ -215,6 +217,30 @@ internal class MessageListViewModelTest { 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) @@ -360,6 +386,10 @@ internal class MessageListViewModelTest { 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, From 411b1bcdaba12dc0d217054c542983319dba0d23 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Wed, 27 May 2026 11:47:39 +0200 Subject: [PATCH 14/14] Add CreatePollViewModel test for config propagation --- .../build.gradle.kts | 1 + .../messages/CreatePollViewModelTest.kt | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/viewmodels/messages/CreatePollViewModelTest.kt 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/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() + } +}