From 04a7590804ed3f61ee876c0a5f8367241a84c73b Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 9 Dec 2021 15:09:12 +0300 Subject: [PATCH] Code review fixes. --- .../session/room/model/PollSummaryContent.kt | 20 ++++----- .../room/model/message/MessagePollContent.kt | 4 ++ .../message/MessagePollResponseContent.kt | 4 ++ .../session/room/model/message/MessageType.kt | 8 +++- .../sdk/api/session/room/send/SendService.kt | 4 +- .../EventRelationsAggregationProcessor.kt | 42 +++++++++++++++---- .../session/room/send/DefaultSendService.kt | 4 +- .../room/send/LocalEchoEventFactory.kt | 6 +-- .../attachments/AttachmentTypeSelectorView.kt | 3 +- .../home/room/detail/RoomDetailAction.kt | 2 +- .../home/room/detail/RoomDetailViewModel.kt | 8 ++-- .../helper/MessageInformationDataFactory.kt | 12 ++++-- .../timeline/item/MessageInformationData.kt | 10 ++++- .../room/detail/timeline/item/PollItem.kt | 13 +++--- .../src/main/res/layout/item_poll_option.xml | 8 ++-- vector/src/main/res/values/strings.xml | 20 ++++----- 16 files changed, 111 insertions(+), 57 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt index bcc354f008..f1e4354314 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt @@ -27,17 +27,17 @@ data class PollSummaryContent( var myVote: String? = null, // Array of VoteInfo, list is constructed so that there is only one vote by user // And that optionIndex is valid - var votes: List? = null -) { + var votes: List? = null, + var votesSummary: Map? = null, + var totalVotes: Int = 0, + var winnerVoteCount: Int = 0 +) - fun voteCount(): Int { - return votes?.size ?: 0 - } - - fun voteCountForOption(option: String): Int { - return votes?.filter { it.option == option }?.count() ?: 0 - } -} +@JsonClass(generateAdapter = true) +data class VoteSummary( + val total: Int = 0, + val percentage: Double = 0.0 +) @JsonClass(generateAdapter = true) data class VoteInfo( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt index 445c3849f5..a4e1317290 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt @@ -23,6 +23,10 @@ import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultCon @JsonClass(generateAdapter = true) data class MessagePollContent( + /** + * Local message type, not from server + */ + @Transient override val msgType: String = MessageType.MSGTYPE_POLL_START, @Json(name = "body") override val body: String = "", @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt index f7458cc25c..f3b4e3dc23 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt @@ -23,6 +23,10 @@ import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultCon @JsonClass(generateAdapter = true) data class MessagePollResponseContent( + /** + * Local message type, not from server + */ + @Transient override val msgType: String = MessageType.MSGTYPE_POLL_RESPONSE, @Json(name = "body") override val body: String = "", @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt index 9697493201..2a6138ae60 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -25,14 +25,18 @@ object MessageType { const val MSGTYPE_VIDEO = "m.video" const val MSGTYPE_LOCATION = "m.location" 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" // 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 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_SNOWFALL = "io.element.effect.snowfall" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt index 4c4b3c7847..5b387c3413 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt @@ -91,10 +91,10 @@ interface SendService { /** * Method to send a poll response. * @param pollEventId the poll currently replied to - * @param optionKey The option key + * @param answerId The id of the answer * @return a [Cancelable] */ - fun registerVoteToPoll(pollEventId: String, optionKey: String): Cancelable + fun voteToPoll(pollEventId: String, answerId: String): Cancelable /** * End a poll in the room. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 95de6fe9b7..66fb0362e3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -17,6 +17,8 @@ package org.matrix.android.sdk.internal.session.room import io.realm.Realm 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.Event 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.toModel 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.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.MessageEndPollContent 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.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.verification.toState 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.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import timber.log.Timber import javax.inject.Inject internal class EventRelationsAggregationProcessor @Inject constructor( - @UserId private val userId: String + @UserId private val userId: String, + private val stateEventDataSource: StateEventDataSource ) : EventInsertLiveProcessor { private val allowedTypes = listOf( @@ -111,9 +118,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! 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}") // A replace! handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) - } else if (it is MessagePollResponseContent) { - Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") - handleResponse(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + } else if (event.getClearType() == EventType.POLL_RESPONSE) { + event.getClearContent().toModel(catchError = true)?.let { pollResponseContent -> + 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) { @@ -372,6 +378,20 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ") } 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) { existingPollSummary.sourceLocalEchoEvents.add(eventId) } else { @@ -405,6 +425,14 @@ internal class EventRelationsAggregationProcessor @Inject constructor( return } + val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition) + ?.content?.toModel() + ?.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 // is it a remote echo? if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 0dc0cefec0..d3162aef79 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -103,8 +103,8 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } - override fun registerVoteToPoll(pollEventId: String, optionKey: String): Cancelable { - return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, optionKey) + override fun voteToPoll(pollEventId: String, answerId: String): Cancelable { + return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId) .also { createLocalEcho(it) } .let { sendEvent(it) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index b0787e713c..31ced37f78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -126,14 +126,14 @@ internal class LocalEchoEventFactory @Inject constructor( fun createPollReplyEvent(roomId: String, pollEventId: String, - optionLabel: String): Event { + answerId: String): Event { val content = MessagePollResponseContent( - body = optionLabel, + body = answerId, relatesTo = RelationDefaultContent( type = RelationType.REFERENCE, eventId = pollEventId), response = PollResponse( - answers = listOf(optionLabel) + answers = listOf(answerId) ) ) diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index 143cc5b031..ccc07ef118 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -38,7 +38,6 @@ import androidx.core.view.isVisible import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.util.ColorGenerator import im.vector.app.R -import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.getMeasurements import im.vector.app.core.utils.PERMISSIONS_EMPTY import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT @@ -132,7 +131,7 @@ class AttachmentTypeSelectorView(context: Context, Type.AUDIO -> views.attachmentAudioButtonContainer Type.CONTACT -> views.attachmentContactButtonContainer Type.POLL -> views.attachmentPollButtonContainer - }.exhaustive.let { + }.let { it.isVisible = isVisible } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index a8a38fbf53..f20a32848c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -52,7 +52,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class RemoveFailedEcho(val eventId: String) : 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( val eventId: String, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 50ad6ad77e..80d386a910 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -289,7 +289,7 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() - is RoomDetailAction.RegisterVoteToPoll -> handleRegisterVoteToPoll(action) + is RoomDetailAction.VoteToPoll -> handleVoteToPoll(action) is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) is RoomDetailAction.RequestVerification -> handleRequestVerification(action) @@ -908,10 +908,10 @@ class RoomDetailViewModel @AssistedInject constructor( } } - private fun handleRegisterVoteToPoll(action: RoomDetailAction.RegisterVoteToPoll) { - // Do not allow to reply to unsent local echo + private fun handleVoteToPoll(action: RoomDetailAction.VoteToPoll) { + // Do not allow to vote unsent local echo of the poll event if (LocalEcho.isLocalEchoId(action.eventId)) return - room.registerVoteToPoll(action.eventId, action.optionKey) + room.voteToPoll(action.eventId, action.optionKey) } private fun handleEndPoll(eventId: String) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index b4ed0c3e94..b30286163e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -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.MessageInformationData 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.ReferencesInfoData import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration @@ -108,9 +109,14 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses PollResponseData( myVote = it.aggregatedContent?.myVote, isClosed = it.closedTime != null, - votes = it.aggregatedContent?.votes - ?.groupBy({ it.option }, { it.userId }) - ?.mapValues { it.value.size } + votes = it.aggregatedContent?.votesSummary?.mapValues { votesSummary -> + PollVoteSummaryData( + total = votesSummary.value.total, + percentage = votesSummary.value.percentage + ) + }, + winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0, + totalVotes = it.aggregatedContent?.totalVotes ?: 0 ) }, hasBeenEdited = event.hasBeenEdited(), diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index 125133d3f7..8258f797f1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -72,10 +72,18 @@ data class ReadReceiptData( @Parcelize data class PollResponseData( val myVote: String?, - val votes: Map?, + val votes: Map?, + val totalVotes: Int = 0, + val winnerVoteCount: Int = 0, val isClosed: Boolean = false ) : Parcelable +@Parcelize +data class PollVoteSummaryData( + val total: Int = 0, + val percentage: Double = 0.0 +) : Parcelable + enum class E2EDecoration { NONE, WARN_IN_CLEAR, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 6f8b6cd9fb..80cb79ac77 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -54,13 +54,14 @@ abstract class PollItem : AbsMessageItem() { val isEnded = pollResponseSummary?.isClosed.orFalse() val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse() val showVotes = didUserVoted || isEnded - val totalVotes = pollResponseSummary?.votes?.map { it.value }?.sum() ?: 0 - val winnerVoteCount = pollResponseSummary?.votes?.map { it.value }?.maxOrNull() ?: -1 + val totalVotes = pollResponseSummary?.totalVotes ?: 0 + val winnerVoteCount = pollResponseSummary?.winnerVoteCount pollContent?.pollCreationInfo?.answers?.forEach { option -> - val isMyVote = pollResponseSummary?.myVote?.let { option.id == it }.orFalse() - val voteCount = pollResponseSummary?.votes?.get(option.id) ?: 0 - val votePercentage = if (voteCount == 0 && totalVotes == 0) 0.0 else voteCount.toDouble() / totalVotes + val voteSummary = pollResponseSummary?.votes?.get(option.id) + val isMyVote = pollResponseSummary?.myVote == option.id + val voteCount = voteSummary?.total ?: 0 + val votePercentage = voteSummary?.percentage ?: 0.0 holder.optionsContainer.addView( PollOptionItem(holder.view.context).apply { @@ -73,7 +74,7 @@ abstract class PollItem : AbsMessageItem() { votePercentage = votePercentage, callback = object : PollOptionItem.Callback { override fun onOptionClicked() { - callback?.onTimelineItemAction(RoomDetailAction.RegisterVoteToPoll(relatedEventId, option.id ?: "")) + callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, option.id ?: "")) } }) } diff --git a/vector/src/main/res/layout/item_poll_option.xml b/vector/src/main/res/layout/item_poll_option.xml index 6d5dd82dd9..95fb9589cc 100644 --- a/vector/src/main/res/layout/item_poll_option.xml +++ b/vector/src/main/res/layout/item_poll_option.xml @@ -10,12 +10,12 @@ android:id="@+id/optionBorderImageView" android:layout_width="0dp" android:layout_height="0dp" + android:importantForAccessibility="no" android:src="@drawable/bg_poll_option" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:ignore="ContentDescription" /> + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" /> At least %1$s options are required - %1$s vote - %1$s votes + %1$d vote + %1$d votes - Based on %1$s vote - Based on %1$s votes + Based on %1$d vote + Based on %1$d votes No votes cast - %1$s vote cast. Vote to the see the results - %1$s votes cast. Vote to the see the results + %1$d vote cast. Vote to the see the results + %1$d votes cast. Vote to the see the results - Final result based on %1$s vote - Final result based on %1$s votes + Final result based on %1$d vote + Final result based on %1$d votes End poll winner option - End poll - 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. + End this poll? + This will stop people from being able to vote and will display the final results of the poll. End poll Enable Polls Vote casted