Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,52 @@
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
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
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() {

private var _binding: StreamUiFragmentCreatePollBinding? = null
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

Expand All @@ -74,13 +88,60 @@ 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() {
Comment thread
gpunto marked this conversation as resolved.
// 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) {
Comment on lines +103 to +106
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're introducing a new edge case here.

If we have multipleVotes.defaultValue = true + multipleVotes.configurable = false, then we end up in a state where:

  • In the VM, allowMultipleVotes = true since we set it here
  • multipleAnswersCount is hidden because of configurable = false
  • sendMenuItem is disabled because of the above (due to this logic) so the user can never create the poll

In Compose v7 we changed the behavior to allow multipleVotes = true + maxVotes = null which means unlimited votes, so maybe we could do something like that? It's a behavior change, though.

Another option is to disallow the problematic combo, e.g. we could add a check(multipleVotes.configurable == true || multipleVotes.defaultValue == false) call in PollsConfig's constructor.

cc @andremion @VelikovPetar wdyt?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am more in favour of disallowing the problematic combination - I wouldn't want to change the default behaviour in a minor release. But once we port this addition to V7, I would say to align it with the compose implementation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you be more explicit at what you'd like to see here?

another option would be to remove the multiple votes configuration from this pr. I included it for completeness, but it is not a feature my team needs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also leave it included for completeness. Basically the suggestion is to put this inside PollsConfig constructor:

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."
    }
}

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
}
}

/**
* Initializes the dialog.
*/
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)
Expand All @@ -97,6 +158,9 @@ public class CreatePollDialogFragment : AppCompatDialogFragment() {
binding.suggestAnOptionSwitch.setOnCheckedChangeListener { _, isChecked ->
createPollViewModel.setSuggestAnOption(isChecked)
}
binding.addACommentLabelSwitch.setOnCheckedChangeListener { _, isChecked ->
createPollViewModel.setAllowAnswers(isChecked)
}
Comment on lines +161 to +163
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this, but it is more or less unrelated to this pr. I think the current functionality(before this pr) is that this switch doesn't do anything. I wasn't able to get this to work in my testing with the changes in this pr either though, so I'm curious what your thoughts are.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It definitely looks like we had a bug both on the creation side and the consuming side, since we never read "allow answers" when displaying poll messages. We'll check that on our side, thanks for fixing the problem at creation time!

binding.addOption.setOnClickListener {
createPollViewModel.createOption()
}
Expand Down Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class CreatePollViewModel : ViewModel() {
private val createPoll = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
private var suggestAnOption = false
private var annonymousPoll = false
private var allowAnswers = false
private var allowMultipleVotes = MutableStateFlow(false)
private var maxAnswers: MutableStateFlow<Int?> = MutableStateFlow(null)

Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<PollAnswer, OptionsAdapter.OptionViewHolder>(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)
Comment thread
gpunto marked this conversation as resolved.

init {
setHasStableIds(true)
}
Expand All @@ -39,6 +48,7 @@ public class OptionsAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionViewHolder =
OptionViewHolder(
parent = parent,
optionTextLimit = optionTextLimit,
onOptionChange = onOptionChange,
)

Expand All @@ -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 */ }
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
}
}
Original file line number Diff line number Diff line change
@@ -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(
Comment thread
gpunto marked this conversation as resolved.
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()
}
}
Loading