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 f5815d7f59b..c7ff0c5b689 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 @@ -30,6 +30,7 @@ import io.getstream.chat.android.ui.common.helper.VideoHeadersProvider import io.getstream.chat.android.ui.common.images.internal.StreamImageLoader 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 @@ -273,6 +274,15 @@ public object ChatUI { @JvmStatic public var showOriginalTranslationEnabled: Boolean = false + /** + * 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 a1a9ebaf5d9..c70bc72773b 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.PollConfig +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 @@ -37,9 +40,12 @@ import io.getstream.chat.android.ui.utils.extensions.applyEdgeToEdgePadding import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlin.jvm.java /** * Represent the bottom sheet dialog that allows users to pick attachments. + * + * Use [newInstance] to create an instance with optional [PollsConfig]. */ public class CreatePollDialogFragment : AppCompatDialogFragment() { @@ -47,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 @@ -74,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 + } } /** @@ -81,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) @@ -97,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() } @@ -158,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 26328e8a0a6..3240b306c68 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) @@ -102,6 +103,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, ) @@ -213,4 +215,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..3a6b2364d11 --- /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,34 @@ +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, + ) + } +} \ No newline at end of file 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..152a745569a --- /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,47 @@ +/* + * 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 { + public companion object { + /** + * The default configuration for polls. All features are configurable and disabled by default. + */ + public val Default: PollsConfig = PollsConfig() + } +}