feat: Allow user to see poll results before voting (#543)

Show a labelled checkbox to the bottom-right of polls that the user has
not voted in and that have votes. If checked the current vote tally (as
percentages) will be shown, along with a bar showing the relative value
of each option.
This commit is contained in:
Nik Clayton 2024-03-19 13:23:21 +01:00 committed by GitHub
parent b478f38e19
commit c2fc3d1f08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 238 additions and 88 deletions

View File

@ -751,7 +751,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="624" line="625"
column="5"/> column="5"/>
</issue> </issue>
@ -1543,7 +1543,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="540" line="541"
column="13"/> column="13"/>
</issue> </issue>
@ -1554,7 +1554,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="581" line="582"
column="13"/> column="13"/>
</issue> </issue>
@ -1565,7 +1565,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="587" line="588"
column="13"/> column="13"/>
</issue> </issue>
@ -1576,7 +1576,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="616" line="617"
column="13"/> column="13"/>
</issue> </issue>
@ -1587,7 +1587,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="643" line="644"
column="13"/> column="13"/>
</issue> </issue>
@ -2720,6 +2720,17 @@
column="9"/> column="9"/>
</issue> </issue>
<issue
id="RtlSymmetry"
message="When you define `paddingEnd` you should probably also define `paddingStart` for right-to-left symmetry"
errorLine1=" android:paddingEnd=&quot;6dp&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/item_poll.xml"
line="36"
column="9"/>
</issue>
<issue <issue
id="RtlSymmetry" id="RtlSymmetry"
message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry"

View File

@ -17,7 +17,6 @@
package app.pachli.adapter package app.pachli.adapter
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import app.pachli.core.activity.emojify import app.pachli.core.activity.emojify
@ -28,7 +27,15 @@ import app.pachli.databinding.ItemPollBinding
import app.pachli.viewdata.PollOptionViewData import app.pachli.viewdata.PollOptionViewData
import app.pachli.viewdata.buildDescription import app.pachli.viewdata.buildDescription
import app.pachli.viewdata.calculatePercent import app.pachli.viewdata.calculatePercent
import com.google.android.material.R
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import kotlin.properties.Delegates
/** Listener for user clicks on poll items */
typealias PollOptionClickListener = (List<PollOptionViewData>) -> Unit
/** Listener for user clicks on results */
typealias ResultClickListener = () -> Unit
// This can't take [app.pachli.viewdata.PollViewData] as a parameter as it also needs to show // This can't take [app.pachli.viewdata.PollViewData] as a parameter as it also needs to show
// data from polls that have been edited, and the "shape" of that data is quite different (no // data from polls that have been edited, and the "shape" of that data is quite different (no
@ -43,9 +50,9 @@ class PollAdapter(
/** True if the user can vote in this poll, false otherwise (e.g., it's from an edit) */ /** True if the user can vote in this poll, false otherwise (e.g., it's from an edit) */
val enabled: Boolean = true, val enabled: Boolean = true,
/** Listener to call when the user clicks on the poll results */ /** Listener to call when the user clicks on the poll results */
private val resultClickListener: View.OnClickListener? = null, private val resultClickListener: ResultClickListener? = null,
/** Listener to call when the user clicks on a poll option */ /** Listener to call when the user clicks on a poll option */
private val pollOptionClickListener: View.OnClickListener? = null, private val pollOptionClickListener: PollOptionClickListener? = null,
) : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() { ) : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
/** How to display a poll */ /** How to display a poll */
@ -60,6 +67,14 @@ class PollAdapter(
MULTIPLE_CHOICE, MULTIPLE_CHOICE,
} }
/**
* True if the poll's current vote details should be shown with the controls to
* vote, false otherwise. Ignored if the display maode is [DisplayMode.RESULT]
*/
var showVotes: Boolean by Delegates.observable(false) { _, _, _ ->
notifyItemRangeChanged(0, itemCount)
}
/** @return the indices of the selected options */ /** @return the indices of the selected options */
fun getSelected() = options.withIndex().filter { it.value.selected }.map { it.index } fun getSelected() = options.withIndex().filter { it.value.selected }.map { it.index }
@ -92,47 +107,66 @@ class PollAdapter(
checkBox.setTextColor(defaultTextColor) checkBox.setTextColor(defaultTextColor)
} }
when (displayMode) { val percent = calculatePercent(option.votesCount, votersCount, votesCount)
DisplayMode.RESULT -> { val level: Int
val percent = calculatePercent(option.votesCount, votersCount, votesCount) val tintColor: Int
resultTextView.text = buildDescription(option.title, percent, option.voted, resultTextView.context) val textColor: Int
val itemText: CharSequence
when {
displayMode == DisplayMode.RESULT && option.voted -> {
level = percent * 100
tintColor = MaterialColors.getColor(resultTextView, R.attr.colorPrimaryContainer)
textColor = MaterialColors.getColor(resultTextView, R.attr.colorOnPrimaryContainer)
itemText = buildDescription(option.title, percent, option.voted, resultTextView.context)
.emojify(emojis, resultTextView, animateEmojis) .emojify(emojis, resultTextView, animateEmojis)
val level = percent * 100
val optionColor: Int
val textColor: Int
// Use the "container" colours to ensure the text is visible on the container
// and on the background, per https://github.com/pachli/pachli-android/issues/85
if (option.voted) {
optionColor = MaterialColors.getColor(resultTextView, com.google.android.material.R.attr.colorPrimaryContainer)
textColor = MaterialColors.getColor(resultTextView, com.google.android.material.R.attr.colorOnPrimaryContainer)
} else {
optionColor = MaterialColors.getColor(resultTextView, com.google.android.material.R.attr.colorSecondaryContainer)
textColor = MaterialColors.getColor(resultTextView, com.google.android.material.R.attr.colorOnSecondaryContainer)
}
resultTextView.background.level = level
resultTextView.background.setTint(optionColor)
resultTextView.setTextColor(textColor)
resultTextView.setOnClickListener(resultClickListener)
} }
DisplayMode.SINGLE_CHOICE -> { displayMode == DisplayMode.RESULT || showVotes -> {
radioButton.text = option.title.emojify(emojis, radioButton, animateEmojis) level = percent * 100
radioButton.isChecked = option.selected tintColor = MaterialColors.getColor(resultTextView, R.attr.colorSecondaryContainer)
radioButton.setOnClickListener { textColor = MaterialColors.getColor(resultTextView, R.attr.colorOnSecondaryContainer)
itemText = buildDescription(option.title, percent, option.voted, resultTextView.context)
.emojify(emojis, resultTextView, animateEmojis)
}
else -> {
level = 0
tintColor = MaterialColors.getColor(resultTextView, R.attr.colorSecondaryContainer)
textColor = MaterialColors.getColor(resultTextView, android.R.attr.textColorPrimary)
itemText = option.title.emojify(emojis, radioButton, animateEmojis)
}
}
when (displayMode) {
DisplayMode.RESULT -> with(resultTextView) {
text = itemText
background.level = level
background.setTint(tintColor)
setTextColor(textColor)
setOnClickListener { resultClickListener?.invoke() }
}
DisplayMode.SINGLE_CHOICE -> with(radioButton) {
isChecked = option.selected
text = itemText
background.level = level
background.setTint(tintColor)
setTextColor(textColor)
setOnClickListener {
options.forEachIndexed { index, pollOption -> options.forEachIndexed { index, pollOption ->
pollOption.selected = index == holder.bindingAdapterPosition pollOption.selected = index == holder.bindingAdapterPosition
notifyItemChanged(index) notifyItemChanged(index)
} }
pollOptionClickListener?.onClick(radioButton) pollOptionClickListener?.invoke(options)
} }
} }
DisplayMode.MULTIPLE_CHOICE -> { DisplayMode.MULTIPLE_CHOICE -> with(checkBox) {
checkBox.text = option.title.emojify(emojis, checkBox, animateEmojis) isChecked = option.selected
checkBox.isChecked = option.selected text = itemText
background.level = level
background.setTint(tintColor)
setTextColor(textColor)
checkBox.setOnCheckedChangeListener { _, isChecked -> checkBox.setOnCheckedChangeListener { _, isChecked ->
options[holder.bindingAdapterPosition].selected = isChecked options[holder.bindingAdapterPosition].selected = isChecked
pollOptionClickListener?.onClick(checkBox) pollOptionClickListener?.invoke(options)
} }
} }
} }

View File

@ -19,14 +19,20 @@ package app.pachli.view
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.text.style.ReplacementSpan
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import androidx.constraintlayout.widget.ConstraintLayout
import android.widget.LinearLayout
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import app.pachli.R import app.pachli.R
import app.pachli.adapter.PollAdapter import app.pachli.adapter.PollAdapter
import app.pachli.adapter.PollOptionClickListener
import app.pachli.adapter.ResultClickListener
import app.pachli.core.common.extensions.hide import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show import app.pachli.core.common.extensions.show
import app.pachli.core.common.util.AbsoluteTimeFormatter import app.pachli.core.common.util.AbsoluteTimeFormatter
@ -39,6 +45,13 @@ import app.pachli.viewdata.buildDescription
import app.pachli.viewdata.calculatePercent import app.pachli.viewdata.calculatePercent
import java.text.NumberFormat import java.text.NumberFormat
/**
* @param choices If null the user has clicked on the poll without voting and this
* should be treated as a navigation click. If non-null the user has voted,
* and [choices] contains the option(s) they voted for.
*/
typealias PollClickListener = (choices: List<Int>?) -> Unit
/** /**
* Compound view that displays [PollViewData]. * Compound view that displays [PollViewData].
* *
@ -56,22 +69,12 @@ class PollView @JvmOverloads constructor(
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0, defStyleAttr: Int = 0,
defStyleRes: Int = 0, defStyleRes: Int = 0,
) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) { ) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
fun interface OnClickListener {
/**
* @param choices If null the user has clicked on the poll without voting and this
* should be treated as a navigation click. If non-null the user has voted,
* and [choices] contains the option(s) they voted for.
*/
fun onClick(choices: List<Int>?)
}
val binding: StatusPollBinding val binding: StatusPollBinding
init { init {
val inflater = context.getSystemService(LayoutInflater::class.java) val inflater = context.getSystemService(LayoutInflater::class.java)
binding = StatusPollBinding.inflate(inflater, this) binding = StatusPollBinding.inflate(inflater, this)
orientation = VERTICAL
} }
fun bind( fun bind(
@ -80,12 +83,12 @@ class PollView @JvmOverloads constructor(
statusDisplayOptions: StatusDisplayOptions, statusDisplayOptions: StatusDisplayOptions,
numberFormat: NumberFormat, numberFormat: NumberFormat,
absoluteTimeFormatter: AbsoluteTimeFormatter, absoluteTimeFormatter: AbsoluteTimeFormatter,
listener: OnClickListener, listener: PollClickListener,
) { ) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
var displayMode: PollAdapter.DisplayMode = PollAdapter.DisplayMode.RESULT var displayMode: PollAdapter.DisplayMode = PollAdapter.DisplayMode.RESULT
var resultClickListener: View.OnClickListener? = null var resultClickListener: ResultClickListener? = null
var pollOptionClickListener: View.OnClickListener? = null var pollOptionClickListener: PollOptionClickListener? = null
// Translated? Create new options from old, using the translated title // Translated? Create new options from old, using the translated title
val options = pollViewData.translatedPoll?.let { val options = pollViewData.translatedPoll?.let {
@ -96,13 +99,14 @@ class PollView @JvmOverloads constructor(
val canVote = !(pollViewData.expired(now) || pollViewData.voted) val canVote = !(pollViewData.expired(now) || pollViewData.voted)
if (canVote) { if (canVote) {
pollOptionClickListener = View.OnClickListener { pollOptionClickListener = {
binding.statusPollButton.isEnabled = options.firstOrNull { it.selected } != null binding.statusPollVoteButton.isEnabled = it.any { it.selected }
} }
displayMode = if (pollViewData.multiple) PollAdapter.DisplayMode.MULTIPLE_CHOICE else PollAdapter.DisplayMode.SINGLE_CHOICE displayMode = if (pollViewData.multiple) PollAdapter.DisplayMode.MULTIPLE_CHOICE else PollAdapter.DisplayMode.SINGLE_CHOICE
} else { } else {
resultClickListener = View.OnClickListener { listener.onClick(null) } resultClickListener = { listener(null) }
binding.statusPollButton.hide() binding.statusPollVoteButton.hide()
binding.statusPollShowResults.hide()
} }
val adapter = PollAdapter( val adapter = PollAdapter(
@ -136,17 +140,31 @@ class PollView @JvmOverloads constructor(
if (!canVote) return if (!canVote) return
// Set up voting // Set up voting
binding.statusPollButton.show() with(binding.statusPollVoteButton) {
binding.statusPollButton.isEnabled = false show()
binding.statusPollButton.setOnClickListener { isEnabled = false
val selected = adapter.getSelected() setOnClickListener {
if (selected.isNotEmpty()) listener.onClick(selected) val selected = adapter.getSelected()
if (selected.isNotEmpty()) listener(selected)
}
}
// Set up showing/hiding votes
if (pollViewData.votesCount > 0) {
with(binding.statusPollShowResults) {
show()
isChecked = adapter.showVotes
setOnCheckedChangeListener { _, isChecked ->
adapter.showVotes = isChecked
}
}
} }
} }
fun hide() { fun hide() {
binding.statusPollOptions.hide() binding.statusPollOptions.hide()
binding.statusPollButton.hide() binding.statusPollVoteButton.hide()
binding.statusPollShowResults.hide()
binding.statusPollDescription.hide() binding.statusPollDescription.hide()
} }
@ -222,3 +240,42 @@ class PollView @JvmOverloads constructor(
return context.getString(R.string.description_poll, *args) return context.getString(R.string.description_poll, *args)
} }
} }
/**
* Span to show vote percentages inline in a poll.
*
* Shows the text at 80% of normal size and bold. Text is right-justified in a space guaranteed
* to be large enough to accomodate "100%".
*/
class VotePercentSpan : ReplacementSpan() {
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
paint.textSize *= 0.8f
return paint.measureText(TEMPLATE).toInt()
}
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
text ?: return
val actualText = text.subSequence(start, end).toString()
paint.textSize *= 0.8f
paint.typeface = Typeface.create(paint.typeface, Typeface.BOLD)
// Compute an x-offset for the text so it will be right aligned
val actualTextWidth = paint.measureText(actualText)
val spanWidth = paint.measureText(TEMPLATE)
val xOffset = spanWidth - actualTextWidth
// Compute a new y value so the text will be centre-aligned within the span
val textBounds = Rect()
paint.getTextBounds(actualText, 0, actualText.length, textBounds)
val spanHeight = (bottom - top)
val newY = (spanHeight / 2) + (textBounds.height() / 2)
canvas.drawText(actualText, start, end, x + xOffset, newY.toFloat(), paint)
}
companion object {
/** Span will be sized to be large enough for this text */
private const val TEMPLATE = "100%"
}
}

View File

@ -19,11 +19,12 @@ package app.pachli.viewdata
import android.content.Context import android.content.Context
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import androidx.core.text.parseAsHtml import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
import app.pachli.R import app.pachli.R
import app.pachli.core.network.model.Poll import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.PollOption import app.pachli.core.network.model.PollOption
import app.pachli.core.network.model.TranslatedPoll import app.pachli.core.network.model.TranslatedPoll
import app.pachli.view.VotePercentSpan
import java.util.Date import java.util.Date
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -86,11 +87,11 @@ fun calculatePercent(fraction: Int, totalVoters: Int?, totalVotes: Int): Int {
} }
fun buildDescription(title: String, percent: Int, voted: Boolean, context: Context): Spanned { fun buildDescription(title: String, percent: Int, voted: Boolean, context: Context): Spanned {
val builder = SpannableStringBuilder(context.getString(R.string.poll_percent_format, percent).parseAsHtml()) val percentStr = context.getString(R.string.poll_percent_format, percent)
val builder = SpannableStringBuilder(percentStr)
builder.setSpan(VotePercentSpan(), 0, percentStr.length, SPAN_EXCLUSIVE_EXCLUSIVE)
if (voted) { if (voted) {
builder.append("") builder.append("")
} else {
builder.append(" ")
} }
return builder.append(title) return builder.append(title)
} }

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2024 Pachli Association
~
~ This file is a part of Pachli.
~
~ This program is free software; you can redistribute it and/or modify it under the terms of the
~ GNU General Public License as published by the Free Software Foundation; either version 3 of the
~ License, or (at your option) any later version.
~
~ Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
~ Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with Pachli; if not,
~ see <http://www.gnu.org/licenses>.
-->
<inset
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/poll_option_background"
android:insetTop="3dp"
android:insetBottom="3dp" />

View File

@ -9,7 +9,8 @@
android:id="@+id/status_poll_option_result" android:id="@+id/status_poll_option_result"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="6dp" android:layout_marginTop="2dp"
android:layout_marginBottom="4dp"
android:background="@drawable/poll_option_background" android:background="@drawable/poll_option_background"
android:maxLines="3" android:maxLines="3"
android:ellipsize="end" android:ellipsize="end"
@ -20,24 +21,30 @@
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textColor="?colorOnSecondary" android:textColor="?colorOnSecondary"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
tools:visibility="gone"
tools:text="40%" /> tools:text="40%" />
<RadioButton <RadioButton
android:id="@+id/status_poll_radio_button" android:id="@+id/status_poll_radio_button"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
app:buttonTint="@color/compound_button_color" app:buttonTint="@color/compound_button_color"
android:background="@drawable/poll_option_background_inset"
android:paddingTop="2dp"
android:paddingEnd="6dp"
tools:text="Option 1" /> tools:text="Option 1" />
<CheckBox <CheckBox
android:id="@+id/status_poll_checkbox" android:id="@+id/status_poll_checkbox"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
app:buttonTint="@color/compound_button_color" app:buttonTint="@color/compound_button_color"
android:background="@drawable/poll_option_background_inset"
tools:visibility="gone"
tools:text="Option 1" /> tools:text="Option 1" />
</FrameLayout> </FrameLayout>

View File

@ -213,7 +213,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/status_display_name" app:layout_constraintStart_toStartOf="@id/status_display_name"
app:layout_constraintTop_toBottomOf="@id/status_media_preview_container" app:layout_constraintTop_toBottomOf="@id/status_media_preview_container"
tools:visibility="gone" /> tools:visibility="visible" />
<TextView <TextView
android:id="@+id/translationProvider" android:id="@+id/translationProvider"

View File

@ -15,38 +15,55 @@
~ see <http://www.gnu.org/licenses>. ~ see <http://www.gnu.org/licenses>.
--> -->
<merge <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/status_poll_options" android:id="@+id/status_poll_options"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:nestedScrollingEnabled="false" /> android:nestedScrollingEnabled="false"
tools:listitem="@layout/item_poll"
tools:itemCount="4" />
<Button <Button
android:id="@+id/status_poll_button" android:id="@+id/status_poll_vote_button"
style="@style/AppButton.Outlined" style="@style/AppButton.Outlined"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center"
android:minWidth="150dp"
android:minHeight="0dp"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingTop="4dp" android:paddingTop="4dp"
android:paddingRight="16dp" android:paddingRight="16dp"
android:paddingBottom="4dp" android:paddingBottom="4dp"
android:text="@string/poll_vote" android:text="@string/poll_vote"
android:textSize="?attr/status_text_medium" /> android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toStartOf="@+id/status_poll_show_results"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/status_poll_options"
app:layout_constraintWidth_max="150dp" />
<CheckBox
android:id="@+id/status_poll_show_results"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:text="@string/poll_show_votes"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/status_poll_vote_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/status_poll_vote_button"
app:layout_constraintTop_toTopOf="@+id/status_poll_vote_button"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/status_poll_description" android:id="@+id/status_poll_description"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="6dp" android:layout_marginTop="8dp"
android:textSize="?attr/status_text_medium"
android:textIsSelectable="true" android:textIsSelectable="true"
android:textSize="?attr/status_text_medium"
app:layout_constraintTop_toBottomOf="@id/status_poll_vote_button"
tools:text="7 votes • 7 hours remaining" /> tools:text="7 votes • 7 hours remaining" />
</merge> </merge>

View File

@ -229,7 +229,7 @@
<item>31536000</item> <item>31536000</item>
</integer-array> </integer-array>
<string name="poll_percent_format"><!-- 15% --> &lt;b>%1$d%%&lt;/b></string> <string name="poll_percent_format"><!-- 15% -->%1$d%%</string>
<string-array name="mute_duration_names"> <string-array name="mute_duration_names">
<item>@string/duration_indefinite</item> <item>@string/duration_indefinite</item>

View File

@ -513,6 +513,7 @@
<string name="poll_info_time_absolute">ends at %s</string> <string name="poll_info_time_absolute">ends at %s</string>
<string name="poll_info_closed">closed</string> <string name="poll_info_closed">closed</string>
<string name="poll_vote">Vote</string> <string name="poll_vote">Vote</string>
<string name="poll_show_votes">Show votes</string>
<string name="poll_ended_voted">A poll you have voted in has ended</string> <string name="poll_ended_voted">A poll you have voted in has ended</string>
<string name="poll_ended_created">A poll you created has ended</string> <string name="poll_ended_created">A poll you created has ended</string>
<!--These are for timestamps on polls --> <!--These are for timestamps on polls -->