Skip to content

Commit 6116f9c

Browse files
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.
1 parent 7ad0cda commit 6116f9c

5 files changed

Lines changed: 192 additions & 4 deletions

File tree

stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/ChatUI.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import io.getstream.chat.android.ui.common.helper.VideoHeadersProvider
3030
import io.getstream.chat.android.ui.common.images.internal.StreamImageLoader
3131
import io.getstream.chat.android.ui.common.images.resizing.StreamCdnImageResizing
3232
import io.getstream.chat.android.ui.common.utils.ChannelNameFormatter
33+
import io.getstream.chat.android.ui.feature.messages.composer.attachment.picker.poll.PollsConfig
3334
import io.getstream.chat.android.ui.feature.messages.composer.attachment.preview.AttachmentPreviewFactoryManager
3435
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.AttachmentFactoryManager
3536
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.DefaultQuotedAttachmentMessageFactory
@@ -273,6 +274,15 @@ public object ChatUI {
273274
@JvmStatic
274275
public var showOriginalTranslationEnabled: Boolean = false
275276

277+
/**
278+
* Configuration for poll creation features. Controls which poll features are configurable by the user
279+
* and their default values.
280+
*
281+
* @see PollsConfig
282+
*/
283+
@JvmStatic
284+
public var pollsConfig: PollsConfig = PollsConfig.Default
285+
276286
/**
277287
* Provides a custom renderer for user avatars.
278288
*/

stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollDialogFragment.kt

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package io.getstream.chat.android.ui.feature.messages.composer.attachment.picker.poll
1818

1919
import android.os.Bundle
20+
import android.text.InputFilter
2021
import android.view.LayoutInflater
2122
import android.view.MenuItem
2223
import android.view.View
@@ -30,6 +31,7 @@ import androidx.core.widget.addTextChangedListener
3031
import androidx.fragment.app.viewModels
3132
import androidx.lifecycle.lifecycleScope
3233
import io.getstream.chat.android.models.PollConfig
34+
import io.getstream.chat.android.ui.ChatUI
3335
import io.getstream.chat.android.ui.R
3436
import io.getstream.chat.android.ui.common.utils.PollsConstants
3537
import io.getstream.chat.android.ui.databinding.StreamUiFragmentCreatePollBinding
@@ -40,15 +42,28 @@ import kotlinx.coroutines.launch
4042

4143
/**
4244
* Represent the bottom sheet dialog that allows users to pick attachments.
45+
*
46+
* Use [newInstance] to create an instance with optional [PollsConfig].
4347
*/
4448
public class CreatePollDialogFragment : AppCompatDialogFragment() {
4549

4650
private var _binding: StreamUiFragmentCreatePollBinding? = null
4751
private val binding get() = _binding!!
4852
private var createPollDialogListener: CreatePollDialogListener? = null
4953
private val createPollViewModel: CreatePollViewModel by viewModels()
54+
private val pollsConfig: PollsConfig by lazy {
55+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
56+
arguments?.getParcelable(ARG_POLLS_CONFIG, PollsConfig::class.java)
57+
} else {
58+
@Suppress("DEPRECATION")
59+
arguments?.getParcelable(ARG_POLLS_CONFIG)
60+
} ?: ChatUI.pollsConfig
61+
}
5062
private val optionsAdapter: OptionsAdapter by lazy {
51-
OptionsAdapter { id, text -> createPollViewModel.onOptionTextChanged(id, text) }
63+
OptionsAdapter(
64+
optionTextLimit = pollsConfig.optionTextLimit,
65+
onOptionChange = { id, text -> createPollViewModel.onOptionTextChanged(id, text) },
66+
)
5267
}
5368
private lateinit var sendMenuItem: MenuItem
5469

@@ -76,11 +91,56 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() {
7691
setupDialog()
7792
}
7893

94+
/**
95+
* Configures the visibility and default values of poll features based on [pollsConfig].
96+
*/
97+
private fun configurePollFeatures() {
98+
// Configure multiple votes feature
99+
createPollViewModel.setAllowMultipleVotes(pollsConfig.multipleVotes.defaultValue)
100+
binding.multipleAnswersLabel.isVisible = pollsConfig.multipleVotes.configurable
101+
binding.multipleAnswersSwitch.isVisible = pollsConfig.multipleVotes.configurable
102+
if (pollsConfig.multipleVotes.configurable) {
103+
binding.multipleAnswersSwitch.isChecked = pollsConfig.multipleVotes.defaultValue
104+
binding.multipleAnswersCount.isVisible = pollsConfig.multipleVotes.defaultValue
105+
}
106+
107+
// Configure anonymous poll feature
108+
createPollViewModel.setAnnonymousPoll(pollsConfig.anonymousPoll.defaultValue)
109+
binding.anonymousPollLabel.isVisible = pollsConfig.anonymousPoll.configurable
110+
binding.anonymousPollSwitch.isVisible = pollsConfig.anonymousPoll.configurable
111+
if (pollsConfig.anonymousPoll.configurable) {
112+
binding.anonymousPollSwitch.isChecked = pollsConfig.anonymousPoll.defaultValue
113+
}
114+
115+
// Configure suggest an option feature
116+
createPollViewModel.setSuggestAnOption(pollsConfig.suggestAnOption.defaultValue)
117+
binding.suggestAnOptionLabel.isVisible = pollsConfig.suggestAnOption.configurable
118+
binding.suggestAnOptionSwitch.isVisible = pollsConfig.suggestAnOption.configurable
119+
if (pollsConfig.suggestAnOption.configurable) {
120+
binding.suggestAnOptionSwitch.isChecked = pollsConfig.suggestAnOption.defaultValue
121+
}
122+
123+
// Configure add a comment feature
124+
createPollViewModel.setAllowAnswers(pollsConfig.addComments.defaultValue)
125+
binding.addACommentLabel.isVisible = pollsConfig.addComments.configurable
126+
binding.addACommentLabelSwitch.isVisible = pollsConfig.addComments.configurable
127+
if (pollsConfig.addComments.configurable) {
128+
binding.addACommentLabelSwitch.isChecked = pollsConfig.addComments.defaultValue
129+
}
130+
}
131+
79132
/**
80133
* Initializes the dialog.
81134
*/
82135
private fun setupDialog() {
83136
setupToolbar(binding.toolbar)
137+
pollsConfig.questionTextLimit?.takeIf { it > 0 }?.let { limit ->
138+
binding.question.filters = arrayOf(InputFilter.LengthFilter(limit))
139+
}
140+
141+
// Configure poll feature visibility and default values based on pollsConfig
142+
configurePollFeatures()
143+
84144
binding.multipleAnswersSwitch.setOnCheckedChangeListener { _, isChecked ->
85145
binding.multipleAnswersCount.isVisible = isChecked
86146
createPollViewModel.setAllowMultipleVotes(isChecked)
@@ -97,6 +157,9 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() {
97157
binding.suggestAnOptionSwitch.setOnCheckedChangeListener { _, isChecked ->
98158
createPollViewModel.setSuggestAnOption(isChecked)
99159
}
160+
binding.addACommentLabelSwitch.setOnCheckedChangeListener { _, isChecked ->
161+
createPollViewModel.setAllowAnswers(isChecked)
162+
}
100163
binding.addOption.setOnClickListener {
101164
createPollViewModel.createOption()
102165
}
@@ -158,15 +221,25 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() {
158221

159222
public companion object {
160223
public const val TAG: String = "create_poll_dialog_fragment"
224+
private const val ARG_POLLS_CONFIG: String = "arg_polls_config"
161225

162226
/**
163227
* Creates a new instance of [CreatePollDialogFragment].
164228
*
229+
* @param createPollDialogListener The listener for poll creation events.
230+
* @param pollsConfig Optional configuration for poll features. Defaults to [ChatUI.pollsConfig].
165231
* @return A new instance of [CreatePollDialogFragment].
166232
*/
167-
public fun newInstance(createPollDialogListener: CreatePollDialogListener): CreatePollDialogFragment {
168-
return CreatePollDialogFragment()
169-
.setCreatePollDialogListener(createPollDialogListener)
233+
@JvmOverloads
234+
public fun newInstance(
235+
createPollDialogListener: CreatePollDialogListener,
236+
pollsConfig: PollsConfig? = null,
237+
): CreatePollDialogFragment {
238+
return CreatePollDialogFragment().apply {
239+
arguments = Bundle().apply {
240+
pollsConfig?.let { config -> putParcelable(ARG_POLLS_CONFIG, config as android.os.Parcelable) }
241+
}
242+
}.setCreatePollDialogListener(createPollDialogListener)
170243
}
171244
}
172245

stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/CreatePollViewModel.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class CreatePollViewModel : ViewModel() {
4646
private val createPoll = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
4747
private var suggestAnOption = false
4848
private var annonymousPoll = false
49+
private var allowAnswers = false
4950
private var allowMultipleVotes = MutableStateFlow(false)
5051
private var maxAnswers: MutableStateFlow<Int?> = MutableStateFlow(null)
5152

@@ -102,6 +103,7 @@ public class CreatePollViewModel : ViewModel() {
102103
options = options.map { PollOption(text = it.text) },
103104
votingVisibility = if (annonymousPoll) VotingVisibility.ANONYMOUS else VotingVisibility.PUBLIC,
104105
allowUserSuggestedOptions = suggestAnOption,
106+
allowAnswers = allowAnswers,
105107
maxVotesAllowed = maxAnswers.takeIf { allowMultipleVotes } ?: 1,
106108
enforceUniqueVote = !allowMultipleVotes,
107109
)
@@ -213,4 +215,13 @@ public class CreatePollViewModel : ViewModel() {
213215
public fun setAnnonymousPoll(annonymousPoll: Boolean) {
214216
this.annonymousPoll = annonymousPoll
215217
}
218+
219+
/**
220+
* Set if the poll allows users to add answers/comments.
221+
*
222+
* @param allowAnswers True if the poll allows users to add answers/comments, false otherwise.
223+
*/
224+
public fun setAllowAnswers(allowAnswers: Boolean) {
225+
this.allowAnswers = allowAnswers
226+
}
216227
}

stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/poll/OptionsAdapter.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package io.getstream.chat.android.ui.feature.messages.composer.attachment.picker.poll
1818

1919
import android.text.Editable
20+
import android.text.InputFilter
2021
import android.text.TextWatcher
2122
import android.view.ViewGroup
2223
import androidx.recyclerview.widget.DiffUtil
@@ -27,9 +28,17 @@ import io.getstream.chat.android.ui.databinding.StreamUiPollOptionBinding
2728
import io.getstream.chat.android.ui.utils.extensions.streamThemeInflater
2829

2930
public class OptionsAdapter(
31+
private val optionTextLimit: Int?,
3032
private val onOptionChange: (id: Int, text: String) -> Unit,
3133
) : ListAdapter<PollAnswer, OptionsAdapter.OptionViewHolder>(OptionDiffCallback) {
3234

35+
/**
36+
* Builds an [OptionsAdapter] instance without providing option text limit.
37+
*
38+
* @param onOptionChange Callback invoked when the option text changes.
39+
*/
40+
public constructor(onOptionChange: (id: Int, text: String) -> Unit) : this(null, onOptionChange)
41+
3342
init {
3443
setHasStableIds(true)
3544
}
@@ -39,6 +48,7 @@ public class OptionsAdapter(
3948
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionViewHolder =
4049
OptionViewHolder(
4150
parent = parent,
51+
optionTextLimit = optionTextLimit,
4252
onOptionChange = onOptionChange,
4353
)
4454

@@ -53,11 +63,18 @@ public class OptionsAdapter(
5363
parent,
5464
false,
5565
),
66+
private val optionTextLimit: Int?,
5667
private val onOptionChange: (id: Int, text: String) -> Unit,
5768
) : RecyclerView.ViewHolder(binding.root) {
5869

5970
private lateinit var pollAnswer: PollAnswer
6071

72+
init {
73+
optionTextLimit?.let { limit ->
74+
binding.option.filters = arrayOf(InputFilter.LengthFilter(limit))
75+
}
76+
}
77+
6178
private val textWatcher = object : TextWatcher {
6279
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { /* no-op */ }
6380
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { /* no-op */ }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.chat.android.ui.feature.messages.composer.attachment.picker.poll
18+
19+
import android.os.Parcelable
20+
import kotlinx.parcelize.Parcelize
21+
22+
/**
23+
* Configuration for individual poll entry feature.
24+
*
25+
* @param configurable Indicates whether the poll entry is configurable. When false, the UI element is hidden.
26+
* @param defaultValue Indicates the default value of the poll entry.
27+
*/
28+
@Parcelize
29+
public data class PollsEntryConfig(
30+
val configurable: Boolean,
31+
val defaultValue: Boolean,
32+
) : Parcelable {
33+
public companion object {
34+
/**
35+
* The default configuration for a poll entry. It will make it configurable and disabled by default.
36+
*/
37+
public val Default: PollsEntryConfig = PollsEntryConfig(
38+
configurable = true,
39+
defaultValue = false,
40+
)
41+
42+
/**
43+
* The feature should not be supported, so it is not configurable by the user and hidden from the UI.
44+
*/
45+
public val NotConfigurable: PollsEntryConfig = PollsEntryConfig(
46+
configurable = false,
47+
defaultValue = false,
48+
)
49+
}
50+
}
51+
52+
/**
53+
* The configuration for the various poll features. It determines if the user can or cannot enable certain poll features.
54+
*
55+
* @param multipleVotes Configuration for allowing multiple votes in a poll.
56+
* @param anonymousPoll Configuration for enabling anonymous polls.
57+
* @param suggestAnOption Configuration for allowing users to suggest options in a poll.
58+
* @param addComments Configuration for adding comments to a poll.
59+
* @param questionTextLimit Optional character limit for the poll question. Null means no limit.
60+
* @param optionTextLimit Optional character limit for poll answer options. Null means no limit.
61+
*/
62+
@Parcelize
63+
public data class PollsConfig(
64+
val multipleVotes: PollsEntryConfig = PollsEntryConfig.Default,
65+
val anonymousPoll: PollsEntryConfig = PollsEntryConfig.Default,
66+
val suggestAnOption: PollsEntryConfig = PollsEntryConfig.Default,
67+
val addComments: PollsEntryConfig = PollsEntryConfig.Default,
68+
val questionTextLimit: Int? = null,
69+
val optionTextLimit: Int? = null,
70+
) : Parcelable {
71+
public companion object {
72+
/**
73+
* The default configuration for polls. All features are configurable and disabled by default.
74+
*/
75+
public val Default: PollsConfig = PollsConfig()
76+
}
77+
}

0 commit comments

Comments
 (0)