Code review fixes.

This commit is contained in:
Onuray Sahin 2021-12-09 15:09:12 +03:00
parent 0f11e498a0
commit 04a7590804
16 changed files with 111 additions and 57 deletions

View File

@ -27,17 +27,17 @@ data class PollSummaryContent(
var myVote: String? = null, var myVote: String? = null,
// Array of VoteInfo, list is constructed so that there is only one vote by user // Array of VoteInfo, list is constructed so that there is only one vote by user
// And that optionIndex is valid // And that optionIndex is valid
var votes: List<VoteInfo>? = null var votes: List<VoteInfo>? = null,
) { var votesSummary: Map<String, VoteSummary>? = null,
var totalVotes: Int = 0,
var winnerVoteCount: Int = 0
)
fun voteCount(): Int { @JsonClass(generateAdapter = true)
return votes?.size ?: 0 data class VoteSummary(
} val total: Int = 0,
val percentage: Double = 0.0
fun voteCountForOption(option: String): Int { )
return votes?.filter { it.option == option }?.count() ?: 0
}
}
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VoteInfo( data class VoteInfo(

View File

@ -23,6 +23,10 @@ import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultCon
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessagePollContent( data class MessagePollContent(
/**
* Local message type, not from server
*/
@Transient
override val msgType: String = MessageType.MSGTYPE_POLL_START, override val msgType: String = MessageType.MSGTYPE_POLL_START,
@Json(name = "body") override val body: String = "", @Json(name = "body") override val body: String = "",
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,

View File

@ -23,6 +23,10 @@ import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultCon
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessagePollResponseContent( data class MessagePollResponseContent(
/**
* Local message type, not from server
*/
@Transient
override val msgType: String = MessageType.MSGTYPE_POLL_RESPONSE, override val msgType: String = MessageType.MSGTYPE_POLL_RESPONSE,
@Json(name = "body") override val body: String = "", @Json(name = "body") override val body: String = "",
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,

View File

@ -25,14 +25,18 @@ object MessageType {
const val MSGTYPE_VIDEO = "m.video" const val MSGTYPE_VIDEO = "m.video"
const val MSGTYPE_LOCATION = "m.location" const val MSGTYPE_LOCATION = "m.location"
const val MSGTYPE_FILE = "m.file" const val MSGTYPE_FILE = "m.file"
const val MSGTYPE_POLL_START = "org.matrix.msc3381.poll.start"
const val MSGTYPE_POLL_RESPONSE = "org.matrix.msc3381.poll.response"
const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request" const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request"
// Add, in local, a fake message type in order to StickerMessage can inherit Message class // Add, in local, a fake message type in order to StickerMessage can inherit Message class
// Because sticker isn't a message type but a event type without msgtype field // Because sticker isn't a message type but a event type without msgtype field
const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker"
// Fake message types for poll events to be able to inherit them from MessageContent
// Because poll events are not message events and they don't hanve msgtype field
const val MSGTYPE_POLL_START = "org.matrix.android.sdk.poll.start"
const val MSGTYPE_POLL_RESPONSE = "org.matrix.android.sdk.poll.response"
const val MSGTYPE_CONFETTI = "nic.custom.confetti" const val MSGTYPE_CONFETTI = "nic.custom.confetti"
const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall" const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall"
} }

View File

@ -91,10 +91,10 @@ interface SendService {
/** /**
* Method to send a poll response. * Method to send a poll response.
* @param pollEventId the poll currently replied to * @param pollEventId the poll currently replied to
* @param optionKey The option key * @param answerId The id of the answer
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun registerVoteToPoll(pollEventId: String, optionKey: String): Cancelable fun voteToPoll(pollEventId: String, answerId: String): Cancelable
/** /**
* End a poll in the room. * End a poll in the room.

View File

@ -17,6 +17,8 @@ package org.matrix.android.sdk.internal.session.room
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.api.crypto.VerificationState import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@ -26,13 +28,16 @@ import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.PollSummaryContent import org.matrix.android.sdk.api.session.room.model.PollSummaryContent
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
import org.matrix.android.sdk.api.session.room.model.VoteInfo import org.matrix.android.sdk.api.session.room.model.VoteInfo
import org.matrix.android.sdk.api.session.room.model.VoteSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.verification.toState import org.matrix.android.sdk.internal.crypto.verification.toState
import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.ContentMapper
@ -52,11 +57,13 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class EventRelationsAggregationProcessor @Inject constructor( internal class EventRelationsAggregationProcessor @Inject constructor(
@UserId private val userId: String @UserId private val userId: String,
private val stateEventDataSource: StateEventDataSource
) : EventInsertLiveProcessor { ) : EventInsertLiveProcessor {
private val allowedTypes = listOf( private val allowedTypes = listOf(
@ -111,9 +118,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace! // A replace!
handleReplace(realm, event, content, roomId, isLocalEcho) handleReplace(realm, event, content, roomId, isLocalEcho)
} else if (content is MessagePollResponseContent) {
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
handleResponse(realm, event, content, roomId, isLocalEcho)
} }
} }
@ -143,9 +147,11 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace! // A replace!
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
} else if (it is MessagePollResponseContent) { } else if (event.getClearType() == EventType.POLL_RESPONSE) {
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { pollResponseContent ->
handleResponse(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
}
} }
} }
} else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
@ -372,6 +378,20 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ") Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ")
} }
sumModel.votes = votes sumModel.votes = votes
// Precompute the percentage of votes for all options
val totalVotes = votes.size
sumModel.totalVotes = totalVotes
sumModel.votesSummary = votes
.groupBy({ it.option }, { it.userId })
.mapValues {
VoteSummary(
total = it.value.size,
percentage = if (totalVotes == 0 && it.value.isEmpty()) 0.0 else it.value.size.toDouble() / totalVotes
)
}
sumModel.winnerVoteCount = sumModel.votesSummary?.maxOf { it.value.total } ?: 0
if (isLocalEcho) { if (isLocalEcho) {
existingPollSummary.sourceLocalEchoEvents.add(eventId) existingPollSummary.sourceLocalEchoEvents.add(eventId)
} else { } else {
@ -405,6 +425,14 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
return return
} }
val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
?.content?.toModel<PowerLevelsContent>()
?.let { PowerLevelsHelper(it) }
if (!powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) {
Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId")
return
}
val txId = event.unsignedData?.transactionId val txId = event.unsignedData?.transactionId
// is it a remote echo? // is it a remote echo?
if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {

View File

@ -103,8 +103,8 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) } .let { sendEvent(it) }
} }
override fun registerVoteToPoll(pollEventId: String, optionKey: String): Cancelable { override fun voteToPoll(pollEventId: String, answerId: String): Cancelable {
return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, optionKey) return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId)
.also { createLocalEcho(it) } .also { createLocalEcho(it) }
.let { sendEvent(it) } .let { sendEvent(it) }
} }

View File

@ -126,14 +126,14 @@ internal class LocalEchoEventFactory @Inject constructor(
fun createPollReplyEvent(roomId: String, fun createPollReplyEvent(roomId: String,
pollEventId: String, pollEventId: String,
optionLabel: String): Event { answerId: String): Event {
val content = MessagePollResponseContent( val content = MessagePollResponseContent(
body = optionLabel, body = answerId,
relatesTo = RelationDefaultContent( relatesTo = RelationDefaultContent(
type = RelationType.REFERENCE, type = RelationType.REFERENCE,
eventId = pollEventId), eventId = pollEventId),
response = PollResponse( response = PollResponse(
answers = listOf(optionLabel) answers = listOf(answerId)
) )
) )

View File

@ -38,7 +38,6 @@ import androidx.core.view.isVisible
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.getMeasurements import im.vector.app.core.extensions.getMeasurements
import im.vector.app.core.utils.PERMISSIONS_EMPTY import im.vector.app.core.utils.PERMISSIONS_EMPTY
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
@ -132,7 +131,7 @@ class AttachmentTypeSelectorView(context: Context,
Type.AUDIO -> views.attachmentAudioButtonContainer Type.AUDIO -> views.attachmentAudioButtonContainer
Type.CONTACT -> views.attachmentContactButtonContainer Type.CONTACT -> views.attachmentContactButtonContainer
Type.POLL -> views.attachmentPollButtonContainer Type.POLL -> views.attachmentPollButtonContainer
}.exhaustive.let { }.let {
it.isVisible = isVisible it.isVisible = isVisible
} }
} }

View File

@ -52,7 +52,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class RemoveFailedEcho(val eventId: String) : RoomDetailAction() data class RemoveFailedEcho(val eventId: String) : RoomDetailAction()
data class CancelSend(val eventId: String, val force: Boolean) : RoomDetailAction() data class CancelSend(val eventId: String, val force: Boolean) : RoomDetailAction()
data class RegisterVoteToPoll(val eventId: String, val optionKey: String) : RoomDetailAction() data class VoteToPoll(val eventId: String, val optionKey: String) : RoomDetailAction()
data class ReportContent( data class ReportContent(
val eventId: String, val eventId: String,

View File

@ -289,7 +289,7 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.RegisterVoteToPoll -> handleRegisterVoteToPoll(action) is RoomDetailAction.VoteToPoll -> handleVoteToPoll(action)
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
is RoomDetailAction.RequestVerification -> handleRequestVerification(action) is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
@ -908,10 +908,10 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun handleRegisterVoteToPoll(action: RoomDetailAction.RegisterVoteToPoll) { private fun handleVoteToPoll(action: RoomDetailAction.VoteToPoll) {
// Do not allow to reply to unsent local echo // Do not allow to vote unsent local echo of the poll event
if (LocalEcho.isLocalEchoId(action.eventId)) return if (LocalEcho.isLocalEchoId(action.eventId)) return
room.registerVoteToPoll(action.eventId, action.optionKey) room.voteToPoll(action.eventId, action.optionKey)
} }
private fun handleEndPoll(eventId: String) { private fun handleEndPoll(eventId: String) {

View File

@ -23,6 +23,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFact
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
@ -108,9 +109,14 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
PollResponseData( PollResponseData(
myVote = it.aggregatedContent?.myVote, myVote = it.aggregatedContent?.myVote,
isClosed = it.closedTime != null, isClosed = it.closedTime != null,
votes = it.aggregatedContent?.votes votes = it.aggregatedContent?.votesSummary?.mapValues { votesSummary ->
?.groupBy({ it.option }, { it.userId }) PollVoteSummaryData(
?.mapValues { it.value.size } total = votesSummary.value.total,
percentage = votesSummary.value.percentage
)
},
winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0,
totalVotes = it.aggregatedContent?.totalVotes ?: 0
) )
}, },
hasBeenEdited = event.hasBeenEdited(), hasBeenEdited = event.hasBeenEdited(),

View File

@ -72,10 +72,18 @@ data class ReadReceiptData(
@Parcelize @Parcelize
data class PollResponseData( data class PollResponseData(
val myVote: String?, val myVote: String?,
val votes: Map<String, Int>?, val votes: Map<String, PollVoteSummaryData>?,
val totalVotes: Int = 0,
val winnerVoteCount: Int = 0,
val isClosed: Boolean = false val isClosed: Boolean = false
) : Parcelable ) : Parcelable
@Parcelize
data class PollVoteSummaryData(
val total: Int = 0,
val percentage: Double = 0.0
) : Parcelable
enum class E2EDecoration { enum class E2EDecoration {
NONE, NONE,
WARN_IN_CLEAR, WARN_IN_CLEAR,

View File

@ -54,13 +54,14 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
val isEnded = pollResponseSummary?.isClosed.orFalse() val isEnded = pollResponseSummary?.isClosed.orFalse()
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse() val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
val showVotes = didUserVoted || isEnded val showVotes = didUserVoted || isEnded
val totalVotes = pollResponseSummary?.votes?.map { it.value }?.sum() ?: 0 val totalVotes = pollResponseSummary?.totalVotes ?: 0
val winnerVoteCount = pollResponseSummary?.votes?.map { it.value }?.maxOrNull() ?: -1 val winnerVoteCount = pollResponseSummary?.winnerVoteCount
pollContent?.pollCreationInfo?.answers?.forEach { option -> pollContent?.pollCreationInfo?.answers?.forEach { option ->
val isMyVote = pollResponseSummary?.myVote?.let { option.id == it }.orFalse() val voteSummary = pollResponseSummary?.votes?.get(option.id)
val voteCount = pollResponseSummary?.votes?.get(option.id) ?: 0 val isMyVote = pollResponseSummary?.myVote == option.id
val votePercentage = if (voteCount == 0 && totalVotes == 0) 0.0 else voteCount.toDouble() / totalVotes val voteCount = voteSummary?.total ?: 0
val votePercentage = voteSummary?.percentage ?: 0.0
holder.optionsContainer.addView( holder.optionsContainer.addView(
PollOptionItem(holder.view.context).apply { PollOptionItem(holder.view.context).apply {
@ -73,7 +74,7 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
votePercentage = votePercentage, votePercentage = votePercentage,
callback = object : PollOptionItem.Callback { callback = object : PollOptionItem.Callback {
override fun onOptionClicked() { override fun onOptionClicked() {
callback?.onTimelineItemAction(RoomDetailAction.RegisterVoteToPoll(relatedEventId, option.id ?: "")) callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, option.id ?: ""))
} }
}) })
} }

View File

@ -10,12 +10,12 @@
android:id="@+id/optionBorderImageView" android:id="@+id/optionBorderImageView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:importantForAccessibility="no"
android:src="@drawable/bg_poll_option" android:src="@drawable/bg_poll_option"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent" />
tools:ignore="ContentDescription" />
<ImageView <ImageView
android:id="@+id/optionCheckImageView" android:id="@+id/optionCheckImageView"
@ -23,10 +23,10 @@
android:layout_height="20dp" android:layout_height="20dp"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:importantForAccessibility="no"
android:src="@drawable/poll_option_unchecked" android:src="@drawable/poll_option_unchecked"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent" />
tools:ignore="ContentDescription" />
<TextView <TextView
android:id="@+id/optionNameTextView" android:id="@+id/optionNameTextView"

View File

@ -3654,26 +3654,26 @@
<item quantity="other">At least %1$s options are required</item> <item quantity="other">At least %1$s options are required</item>
</plurals> </plurals>
<plurals name="poll_option_vote_count"> <plurals name="poll_option_vote_count">
<item quantity="one">%1$s vote</item> <item quantity="one">%1$d vote</item>
<item quantity="other">%1$s votes</item> <item quantity="other">%1$d votes</item>
</plurals> </plurals>
<plurals name="poll_total_vote_count_before_ended_and_voted"> <plurals name="poll_total_vote_count_before_ended_and_voted">
<item quantity="one">Based on %1$s vote</item> <item quantity="one">Based on %1$d vote</item>
<item quantity="other">Based on %1$s votes</item> <item quantity="other">Based on %1$d votes</item>
</plurals> </plurals>
<plurals name="poll_total_vote_count_before_ended_and_not_voted"> <plurals name="poll_total_vote_count_before_ended_and_not_voted">
<item quantity="zero">No votes cast</item> <item quantity="zero">No votes cast</item>
<item quantity="one">%1$s vote cast. Vote to the see the results</item> <item quantity="one">%1$d vote cast. Vote to the see the results</item>
<item quantity="other">%1$s votes cast. Vote to the see the results</item> <item quantity="other">%1$d votes cast. Vote to the see the results</item>
</plurals> </plurals>
<plurals name="poll_total_vote_count_after_ended"> <plurals name="poll_total_vote_count_after_ended">
<item quantity="one">Final result based on %1$s vote</item> <item quantity="one">Final result based on %1$d vote</item>
<item quantity="other">Final result based on %1$s votes</item> <item quantity="other">Final result based on %1$d votes</item>
</plurals> </plurals>
<string name="poll_end_action">End poll</string> <string name="poll_end_action">End poll</string>
<string name="a11y_poll_winner_option">winner option</string> <string name="a11y_poll_winner_option">winner option</string>
<string name="end_poll_confirmation_title">End poll</string> <string name="end_poll_confirmation_title">End this poll?</string>
<string name="end_poll_confirmation_description">Are you sure you want to end this poll? This will stop people from being able to vote and will display the final results of the poll.</string> <string name="end_poll_confirmation_description">This will stop people from being able to vote and will display the final results of the poll.</string>
<string name="end_poll_confirmation_approve_button">End poll</string> <string name="end_poll_confirmation_approve_button">End poll</string>
<string name="labs_enable_polls">Enable Polls</string> <string name="labs_enable_polls">Enable Polls</string>
<string name="poll_response_room_list_preview">Vote casted</string> <string name="poll_response_room_list_preview">Vote casted</string>