Implement poll in timeline ui.

This commit is contained in:
Onuray Sahin 2021-12-03 11:39:41 +03:00
parent 2a3a55894f
commit 06485cf5e4
25 changed files with 521 additions and 62 deletions

View File

@ -0,0 +1,22 @@
{
"question": "What type of food should we have at the party?",
"data": [
{
"answer": "Italian \uD83C\uDDEE\uD83C\uDDF9",
"votes": "9 votes"
},
{
"answer": "Chinese \uD83C\uDDE8\uD83C\uDDF3",
"votes": "1 vote"
},
{
"answer": "Brazilian \uD83C\uDDE7\uD83C\uDDF7",
"votes": "0 votes"
},
{
"answer": "French \uD83C\uDDEB\uD83C\uDDF7",
"votes": "15 votes"
}
],
"totalVotes": "Based on 20 votes"
}

View File

@ -53,7 +53,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 ReplyToOptions(val eventId: String, val optionIndex: Int, val optionValue: String) : RoomDetailAction() data class RegisterVoteToPoll(val eventId: String, val optionKey: String) : RoomDetailAction()
data class ReportContent( data class ReportContent(
val eventId: String, val eventId: String,
@ -116,4 +116,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : RoomDetailAction() data class PlayOrPauseVoicePlayback(val eventId: String, val messageAudioContent: MessageAudioContent) : RoomDetailAction()
object PlayOrPauseRecordingPlayback : RoomDetailAction() object PlayOrPauseRecordingPlayback : RoomDetailAction()
data class EndAllVoiceActions(val deleteRecord: Boolean = true) : RoomDetailAction() data class EndAllVoiceActions(val deleteRecord: Boolean = true) : RoomDetailAction()
// Poll
data class EndPoll(val eventId: String) : RoomDetailAction()
} }

View File

@ -2017,6 +2017,9 @@ class RoomDetailFragment @Inject constructor(
startActivity(KeysBackupRestoreActivity.intent(it)) startActivity(KeysBackupRestoreActivity.intent(it))
} }
} }
is EventSharedAction.EndPoll -> {
roomDetailViewModel.handle(RoomDetailAction.EndPoll(action.eventId))
}
} }
} }

View File

@ -309,7 +309,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.ReplyToOptions -> handleReplyToOptions(action) is RoomDetailAction.RegisterVoteToPoll -> handleRegisterVoteToPoll(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)
@ -355,6 +355,7 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
_viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true)) _viewEvents.post(RoomDetailViewEvents.OpenRoom(action.replacementRoomId, closeCurrentRoom = true))
} }
is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId)
}.exhaustive }.exhaustive
} }
@ -983,10 +984,14 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun handleReplyToOptions(action: RoomDetailAction.ReplyToOptions) { private fun handleRegisterVoteToPoll(action: RoomDetailAction.RegisterVoteToPoll) {
// Do not allow to reply to unsent local echo // Do not allow to reply to unsent local echo
if (LocalEcho.isLocalEchoId(action.eventId)) return if (LocalEcho.isLocalEchoId(action.eventId)) return
room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue) room.registerVoteToPoll(action.eventId, action.optionKey)
}
private fun handleEndPoll(eventId: String) {
room.endPoll(eventId)
} }
private fun observeSyncState() { private fun observeSyncState() {

View File

@ -23,6 +23,7 @@ import android.text.style.AbsoluteSizeSpan
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.view.View import android.view.View
import com.airbnb.epoxy.EpoxyModel
import dagger.Lazy import dagger.Lazy
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
@ -48,12 +49,12 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem_
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.MessageOptionsItem_
import im.vector.app.features.home.room.detail.timeline.item.MessagePollItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_
import im.vector.app.features.home.room.detail.timeline.item.PollItem
import im.vector.app.features.home.room.detail.timeline.item.PollItem_
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_ import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
@ -80,14 +81,11 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageEmoteContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL
import org.matrix.android.sdk.api.session.room.model.message.getFileName import org.matrix.android.sdk.api.session.room.model.message.getFileName
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
@ -125,7 +123,7 @@ class MessageItemFactory @Inject constructor(
pillsPostProcessorFactory.create(roomId) pillsPostProcessorFactory.create(roomId)
} }
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { fun create(params: TimelineItemFactoryParams): EpoxyModel<*>? {
val event = params.event val event = params.event
val highlight = params.isHighlighted val highlight = params.isHighlighted
val callback = params.callback val callback = params.callback
@ -168,41 +166,24 @@ class MessageItemFactory @Inject constructor(
} }
} }
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) is MessagePollContent -> buildPollContent(messageContent, informationData, highlight, callback, attributes)
is MessagePollResponseContent -> noticeItemFactory.create(params)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
} }
} }
private fun buildOptionsMessageItem(messageContent: MessageOptionsContent, private fun buildPollContent(messageContent: MessagePollContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { attributes: AbsMessageItem.Attributes): PollItem? {
return when (messageContent.optionType) { return PollItem_()
OPTION_TYPE_POLL -> {
MessagePollItem_()
.attributes(attributes) .attributes(attributes)
.callback(callback) .eventId(informationData.eventId)
.informationData(informationData) .pollResponseSummary(informationData.pollResponseAggregatedSummary)
.leftGuideline(avatarSizeProvider.leftGuideline) .pollContent(messageContent)
.optionsContent(messageContent)
.highlighted(highlight) .highlighted(highlight)
}
OPTION_TYPE_BUTTONS -> {
MessageOptionsItem_()
.attributes(attributes)
.callback(callback)
.informationData(informationData)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.optionsContent(messageContent) .callback(callback)
.highlighted(highlight)
}
else -> {
// Not supported optionType
buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
}
}
} }
private fun buildAudioMessageItem(messageContent: MessageAudioContent, private fun buildAudioMessageItem(messageContent: MessageAudioContent,

View File

@ -48,6 +48,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
when (event.root.getClearType()) { when (event.root.getClearType()) {
// Message itemsX // Message itemsX
EventType.STICKER, EventType.STICKER,
EventType.POLL_START,
EventType.MESSAGE -> messageItemFactory.create(params) EventType.MESSAGE -> messageItemFactory.create(params)
EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,

View File

@ -107,9 +107,9 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let { pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let {
PollResponseData( PollResponseData(
myVote = it.aggregatedContent?.myVote, myVote = it.aggregatedContent?.myVote,
isClosed = it.closedTime ?: Long.MAX_VALUE > System.currentTimeMillis(), isClosed = it.closedTime != null,
votes = it.aggregatedContent?.votes votes = it.aggregatedContent?.votes
?.groupBy({ it.optionIndex }, { it.userId }) ?.groupBy({ it.option }, { it.userId })
?.mapValues { it.value.size } ?.mapValues { it.value.size }
) )
}, },

View File

@ -50,7 +50,8 @@ object TimelineDisplayableEvents {
EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_JOIN_RULES, EventType.STATE_ROOM_JOIN_RULES,
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL EventType.KEY_VERIFICATION_CANCEL,
EventType.POLL_START
) )
} }

View File

@ -71,8 +71,8 @@ data class ReadReceiptData(
@Parcelize @Parcelize
data class PollResponseData( data class PollResponseData(
val myVote: Int?, val myVote: String?,
val votes: Map<Int, Int>?, val votes: Map<String, Int>?,
val isClosed: Boolean = false val isClosed: Boolean = false
) : Parcelable ) : Parcelable

View File

@ -0,0 +1,101 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 im.vector.app.features.home.room.detail.timeline.item
import android.widget.LinearLayout
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
@EpoxyAttribute
var pollContent: MessagePollContent? = null
@EpoxyAttribute
var pollResponseSummary: PollResponseData? = null
@EpoxyAttribute
var callback: TimelineEventController.Callback? = null
@EpoxyAttribute
var eventId: String? = null
override fun bind(holder: Holder) {
super.bind(holder)
val relatedEventId = eventId ?: return
renderSendState(holder.view, holder.questionTextView)
holder.questionTextView.text = pollContent?.pollCreationInfo?.question?.question
holder.optionsContainer.removeAllViews()
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() ?: 0
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
holder.optionsContainer.addView(
PollOptionItem(holder.view.context).apply {
update(optionName = option.answer ?: "",
isSelected = isMyVote,
isWinner = voteCount == winnerVoteCount,
isEnded = isEnded,
showVote = showVotes,
voteCount = voteCount,
votePercentage = votePercentage,
callback = object : PollOptionItem.Callback {
override fun onOptionClicked() {
callback?.onTimelineItemAction(RoomDetailAction.RegisterVoteToPoll(relatedEventId, option.id ?: ""))
}
})
}
)
}
holder.totalVotesTextView.apply {
text = when {
isEnded -> resources.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes)
didUserVoted -> resources.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes)
else -> resources.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, totalVotes, totalVotes)
}
}
}
class Holder : AbsMessageItem.Holder(STUB_ID) {
val questionTextView by bind<TextView>(R.id.questionTextView)
val optionsContainer by bind<LinearLayout>(R.id.optionsContainer)
val totalVotesTextView by bind<TextView>(R.id.optionsTotalVotesTextView)
}
companion object {
private const val STUB_ID = R.id.messageContentPollStub
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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 im.vector.app.features.home.room.detail.timeline.item
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.extensions.setAttributeTintedImageResource
import im.vector.app.databinding.ItemPollOptionBinding
class PollOptionItem @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
interface Callback {
fun onOptionClicked()
}
private lateinit var views: ItemPollOptionBinding
private var callback: Callback? = null
init {
setupViews()
}
private fun setupViews() {
inflate(context, R.layout.item_poll_option, this)
views = ItemPollOptionBinding.bind(this)
views.root.setOnClickListener { callback?.onOptionClicked() }
}
fun update(optionName: String,
isSelected: Boolean,
isWinner: Boolean,
isEnded: Boolean,
showVote: Boolean,
voteCount: Int,
votePercentage: Double,
callback: Callback) {
this.callback = callback
views.optionNameTextView.text = optionName
views.optionCheckImageView.isVisible = !isEnded
if (isEnded && isWinner) {
views.optionBorderImageView.setAttributeTintedImageResource(R.drawable.bg_poll_option, R.attr.colorPrimary)
views.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_checked)
views.optionWinnerImageView.isVisible = true
} else if (isSelected) {
views.optionBorderImageView.setAttributeTintedImageResource(R.drawable.bg_poll_option, R.attr.colorPrimary)
views.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_checked)
views.optionCheckImageView.setImageResource(R.drawable.poll_option_checked)
views.optionWinnerImageView.isVisible = false
} else {
views.optionBorderImageView.setAttributeTintedImageResource(R.drawable.bg_poll_option, R.attr.vctr_content_quinary)
views.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_unchecked)
views.optionCheckImageView.setImageResource(R.drawable.poll_option_unchecked)
views.optionWinnerImageView.isVisible = false
}
if (showVote) {
views.optionVoteCountTextView.apply {
isVisible = true
text = resources.getQuantityString(R.plurals.poll_option_vote_count, voteCount, voteCount)
}
views.optionVoteProgress.apply {
val progressValue = (votePercentage * 100).toInt()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setProgress(progressValue, true)
} else {
progress = progressValue
}
}
} else {
views.optionVoteCountTextView.isVisible = false
views.optionVoteProgress.progress = 0
}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<stroke
android:width="1dp"
android:color="?vctr_content_quinary" />
<corners android:radius="4dp" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size
android:width="1dp"
android:height="16dp" />
</shape>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M20,7L9,18L4,13"
android:strokeWidth="2"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M12.6667,3.3333H11.3333V2.6667C11.3333,2.3 11.0333,2 10.6667,2H5.3333C4.9667,2 4.6667,2.3 4.6667,2.6667V3.3333H3.3333C2.6,3.3333 2,3.9333 2,4.6667V5.3333C2,7.0333 3.28,8.42 4.9267,8.6267C5.3467,9.6267 6.2467,10.38 7.3333,10.6V12.6667H5.3333C4.9667,12.6667 4.6667,12.9667 4.6667,13.3333C4.6667,13.7 4.9667,14 5.3333,14H10.6667C11.0333,14 11.3333,13.7 11.3333,13.3333C11.3333,12.9667 11.0333,12.6667 10.6667,12.6667H8.6667V10.6C9.7533,10.38 10.6533,9.6267 11.0733,8.6267C12.72,8.42 14,7.0333 14,5.3333V4.6667C14,3.9333 13.4,3.3333 12.6667,3.3333ZM3.3333,5.3333V4.6667H4.6667V7.2133C3.8933,6.9333 3.3333,6.2 3.3333,5.3333ZM12.6667,5.3333C12.6667,6.2 12.1067,6.9333 11.3333,7.2133V4.6667H12.6667V5.3333Z"
android:fillColor="#0DBD8B"/>
</vector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="?colorPrimary" />
<size
android:width="20dp"
android:height="20dp" />
</shape>
</item>
<item
android:drawable="@drawable/ic_check_on_white"
android:gravity="center" />
</layer-list>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="4dp" />
<solid android:color="?vctr_system" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="4dp" />
<solid android:color="?colorPrimary" />
</shape>
</clip>
</item>
</layer-list>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="4dp" />
<solid android:color="?vctr_system" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="4dp" />
<solid android:color="?vctr_content_quaternary" />
</shape>
</clip>
</item>
</layer-list>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/transparent" />
<stroke
android:width="1dp"
android:color="?vctr_disabled_view_color" />
<size
android:width="20dp"
android:height="20dp" />
</shape>

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:id="@+id/optionContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/optionBorderImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="64dp"
android:src="@drawable/bg_poll_option"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/optionCheckImageView"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="12dp"
android:layout_marginTop="16dp"
android:src="@drawable/poll_option_unchecked"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/optionNameTextView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="12dp"
app:layout_constraintEnd_toEndOf="@id/optionWinnerImageView"
app:layout_constraintStart_toEndOf="@id/optionCheckImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/poll.json/data/answer" />
<ImageView
android:id="@+id/optionWinnerImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/a11y_poll_winner_option"
android:src="@drawable/ic_poll_winner"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/optionVoteCountTextView"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/optionVoteProgress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/optionVoteProgress"
tools:text="@sample/poll.json/data/votes"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/optionVoteProgress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="6dp"
android:layout_marginStart="8dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="8dp"
android:progressDrawable="@drawable/poll_option_progressbar_checked"
app:layout_constraintEnd_toStartOf="@id/optionVoteCountTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/optionNameTextView"
tools:progress="60" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -118,20 +118,6 @@
android:layout_marginEnd="56dp" android:layout_marginEnd="56dp"
android:layout="@layout/item_timeline_event_redacted_stub" /> android:layout="@layout/item_timeline_event_redacted_stub" />
<ViewStub
android:id="@+id/messagePollStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:layout_marginEnd="56dp"
android:layout="@layout/item_timeline_event_poll_stub" />
<ViewStub
android:id="@+id/messageOptionsStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:layout_marginEnd="56dp"
android:layout="@layout/item_timeline_event_option_buttons_stub" />
<ViewStub <ViewStub
android:id="@+id/messageContentVoiceStub" android:id="@+id/messageContentVoiceStub"
style="@style/TimelineContentStubBaseParams" style="@style/TimelineContentStubBaseParams"
@ -139,6 +125,12 @@
android:layout="@layout/item_timeline_event_voice_stub" android:layout="@layout/item_timeline_event_voice_stub"
tools:visibility="visible" /> tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentPollStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_poll"
tools:visibility="visible" />
</FrameLayout> </FrameLayout>
<im.vector.app.core.ui.views.SendStateImageView <im.vector.app.core.ui.views.SendStateImageView

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/questionTextView"
style="@style/Widget.Vector.TextView.HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/poll.json/question" />
<LinearLayout
android:id="@+id/optionsContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:divider="@drawable/divider_poll_options"
android:orientation="vertical"
android:showDividers="middle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/questionTextView" />
<TextView
android:id="@+id/optionsTotalVotesTextView"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/optionsContainer"
tools:text="@sample/poll.json/totalVotes" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pollItemContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />

View File

@ -171,7 +171,6 @@
android:layout_margin="16dp" android:layout_margin="16dp"
android:baselineAligned="false" android:baselineAligned="false"
android:orientation="horizontal" android:orientation="horizontal"
android:visibility="gone"
android:weightSum="3"> android:weightSum="3">
<LinearLayout <LinearLayout

View File

@ -3648,4 +3648,23 @@
<item quantity="one">At least %1$s option is required</item> <item quantity="one">At least %1$s option is required</item>
<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">
<item quantity="one">%1$s vote</item>
<item quantity="other">%1$s votes</item>
</plurals>
<plurals name="poll_total_vote_count_before_ended_and_voted">
<item quantity="one">Based on %1$s vote</item>
<item quantity="other">Based on %1$s votes</item>
</plurals>
<plurals name="poll_total_vote_count_before_ended_and_not_voted">
<item quantity="zero">No votes cast</item>
<item quantity="one">%1$s vote cast. Vote to the see the results</item>
<item quantity="other">%1$s votes cast. Vote to the see the results</item>
</plurals>
<plurals name="poll_total_vote_count_after_ended">
<item quantity="one">Final result based on %1$s vote</item>
<item quantity="other">Final result based on %1$s votes</item>
</plurals>
<string name="poll_end_action">End poll</string>
<string name="a11y_poll_winner_option">winner option</string>
</resources> </resources>