Undisclosed poll implementation.

This commit is contained in:
Onuray Sahin 2022-01-24 14:31:50 +03:00
parent 7f97e78ba3
commit b0b92c062e
20 changed files with 247 additions and 29 deletions

View File

@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class PollCreationInfo( data class PollCreationInfo(
@Json(name = "question") val question: PollQuestion? = null, @Json(name = "question") val question: PollQuestion? = null,
@Json(name = "kind") val kind: String? = "org.matrix.msc3381.poll.disclosed", @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED,
@Json(name = "max_selections") val maxSelections: Int = 1, @Json(name = "max_selections") val maxSelections: Int = 1,
@Json(name = "answers") val answers: List<PollAnswer>? = null @Json(name = "answers") val answers: List<PollAnswer>? = null
) )

View File

@ -0,0 +1,35 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class PollType {
/**
* Voters should see results as soon as they have voted.
*/
@Json(name = "org.matrix.msc3381.poll.disclosed")
DISCLOSED,
/**
* Results should be only revealed when the poll is ended.
*/
@Json(name = "org.matrix.msc3381.poll.undisclosed")
UNDISCLOSED
}

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.relation
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
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.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.message.PollType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
@ -66,11 +67,13 @@ interface RelationService {
/** /**
* Edit a poll. * Edit a poll.
* @param pollType indicates open or closed polls
* @param targetEvent The poll event to edit * @param targetEvent The poll event to edit
* @param question The edited question * @param question The edited question
* @param options The edited options * @param options The edited options
*/ */
fun editPoll(targetEvent: TimelineEvent, fun editPoll(targetEvent: TimelineEvent,
pollType: PollType,
question: String, question: String,
options: List<String>): Cancelable options: List<String>): Cancelable

View File

@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
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.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.PollType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -91,11 +92,12 @@ interface SendService {
/** /**
* Send a poll to the room. * Send a poll to the room.
* @param pollType indicates open or closed polls
* @param question the question * @param question the question
* @param options list of options * @param options list of options
* @return a [Cancelable] * @return a [Cancelable]
*/ */
fun sendPoll(question: String, options: List<String>): Cancelable fun sendPoll(pollType: PollType, question: String, options: List<String>): Cancelable
/** /**
* Method to send a poll response. * Method to send a poll response.

View File

@ -24,6 +24,7 @@ import dagger.assisted.AssistedInject
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
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.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.message.PollType
import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.model.relation.RelationService
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -113,9 +114,10 @@ internal class DefaultRelationService @AssistedInject constructor(
} }
override fun editPoll(targetEvent: TimelineEvent, override fun editPoll(targetEvent: TimelineEvent,
pollType: PollType,
question: String, question: String,
options: List<String>): Cancelable { options: List<String>): Cancelable {
return eventEditor.editPoll(targetEvent, question, options) return eventEditor.editPoll(targetEvent, pollType, question, options)
} }
override fun editTextMessage(targetEvent: TimelineEvent, override fun editTextMessage(targetEvent: TimelineEvent,

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.relation
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.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.PollType
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -59,17 +60,18 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
} }
fun editPoll(targetEvent: TimelineEvent, fun editPoll(targetEvent: TimelineEvent,
pollType: PollType,
question: String, question: String,
options: List<String>): Cancelable { options: List<String>): Cancelable {
val roomId = targetEvent.roomId val roomId = targetEvent.roomId
if (targetEvent.root.sendState.hasFailed()) { if (targetEvent.root.sendState.hasFailed()) {
val editedEvent = eventFactory.createPollEvent(roomId, question, options).copy( val editedEvent = eventFactory.createPollEvent(roomId, pollType, question, options).copy(
eventId = targetEvent.eventId eventId = targetEvent.eventId
) )
return sendFailedEvent(targetEvent, editedEvent) return sendFailedEvent(targetEvent, editedEvent)
} else if (targetEvent.root.sendState.isSent()) { } else if (targetEvent.root.sendState.isSent()) {
val event = eventFactory val event = eventFactory
.createPollReplaceEvent(roomId, targetEvent.eventId, question, options) .createPollReplaceEvent(roomId, pollType, targetEvent.eventId, question, options)
return sendReplaceEvent(roomId, event) return sendReplaceEvent(roomId, event)
} else { } else {
Timber.w("Can't edit a sending event") Timber.w("Can't edit a sending event")

View File

@ -37,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
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.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.PollType
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.send.SendService import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
@ -103,8 +104,8 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) } .let { sendEvent(it) }
} }
override fun sendPoll(question: String, options: List<String>): Cancelable { override fun sendPoll(pollType: PollType, question: String, options: List<String>): Cancelable {
return localEchoEventFactory.createPollEvent(roomId, question, options) return localEchoEventFactory.createPollEvent(roomId, pollType, question, options)
.also { createLocalEcho(it) } .also { createLocalEcho(it) }
.let { sendEvent(it) } .let { sendEvent(it) }
} }

View File

@ -48,6 +48,7 @@ import org.matrix.android.sdk.api.session.room.model.message.PollAnswer
import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo
import org.matrix.android.sdk.api.session.room.model.message.PollQuestion import org.matrix.android.sdk.api.session.room.model.message.PollQuestion
import org.matrix.android.sdk.api.session.room.model.message.PollResponse import org.matrix.android.sdk.api.session.room.model.message.PollResponse
import org.matrix.android.sdk.api.session.room.model.message.PollType
import org.matrix.android.sdk.api.session.room.model.message.ThumbnailInfo import org.matrix.android.sdk.api.session.room.model.message.ThumbnailInfo
import org.matrix.android.sdk.api.session.room.model.message.VideoInfo import org.matrix.android.sdk.api.session.room.model.message.VideoInfo
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
@ -126,12 +127,14 @@ internal class LocalEchoEventFactory @Inject constructor(
} }
private fun createPollContent(question: String, private fun createPollContent(question: String,
options: List<String>): MessagePollContent { options: List<String>,
pollType: PollType): MessagePollContent {
return MessagePollContent( return MessagePollContent(
pollCreationInfo = PollCreationInfo( pollCreationInfo = PollCreationInfo(
question = PollQuestion( question = PollQuestion(
question = question question = question
), ),
kind = pollType,
answers = options.map { option -> answers = options.map { option ->
PollAnswer( PollAnswer(
id = UUID.randomUUID().toString(), id = UUID.randomUUID().toString(),
@ -143,12 +146,13 @@ internal class LocalEchoEventFactory @Inject constructor(
} }
fun createPollReplaceEvent(roomId: String, fun createPollReplaceEvent(roomId: String,
pollType: PollType,
targetEventId: String, targetEventId: String,
question: String, question: String,
options: List<String>): Event { options: List<String>): Event {
val newContent = MessagePollContent( val newContent = MessagePollContent(
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
newContent = createPollContent(question, options).toContent() newContent = createPollContent(question, options, pollType).toContent()
) )
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
return Event( return Event(
@ -186,9 +190,10 @@ internal class LocalEchoEventFactory @Inject constructor(
} }
fun createPollEvent(roomId: String, fun createPollEvent(roomId: String,
pollType: PollType,
question: String, question: String,
options: List<String>): Event { options: List<String>): Event {
val content = createPollContent(question, options) val content = createPollContent(question, options, pollType)
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
return Event( return Event(
roomId = roomId, roomId = roomId,

View File

@ -89,6 +89,7 @@ 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.PollType
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
@ -186,11 +187,14 @@ class MessageItemFactory @Inject constructor(
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse() val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
val winnerVoteCount = pollResponseSummary?.winnerVoteCount val winnerVoteCount = pollResponseSummary?.winnerVoteCount
val isPollSent = informationData.sendState.isSent() val isPollSent = informationData.sendState.isSent()
val isPollUndisclosed = pollContent.pollCreationInfo?.kind == PollType.UNDISCLOSED
val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let { val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let {
when { when {
isEnded -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, it, it) isEnded -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, it, it)
didUserVoted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, it, it) isPollUndisclosed -> ""
else -> if (it == 0) { didUserVoted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, it, it)
else -> if (it == 0) {
stringProvider.getString(R.string.poll_no_votes_cast) stringProvider.getString(R.string.poll_no_votes_cast)
} else { } else {
stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, it, it) stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, it, it)
@ -214,6 +218,9 @@ class MessageItemFactory @Inject constructor(
// Poll is ended. Disable option, show votes and mark the winner. // Poll is ended. Disable option, show votes and mark the winner.
val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount
PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner) PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner)
} else if (isPollUndisclosed) {
// Poll is closed. Enable option, hide votes and mark the user's selection.
PollOptionViewState.PollUndisclosed(optionId, optionAnswer, isMyVote)
} else if (didUserVoted) { } else if (didUserVoted) {
// User voted to the poll, but poll is not ended. Enable option, show votes and mark the user's selection. // User voted to the poll, but poll is not ended. Enable option, show votes and mark the user's selection.
PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote) PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote)

View File

@ -22,9 +22,9 @@ import androidx.core.view.children
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
@EpoxyModelClass(layout = R.layout.item_timeline_event_base) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class PollItem : AbsMessageItem<PollItem.Holder>() { abstract class PollItem : AbsMessageItem<PollItem.Holder>() {

View File

@ -23,6 +23,7 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.setAttributeTintedImageResource import im.vector.app.core.extensions.setAttributeTintedImageResource
import im.vector.app.databinding.ItemPollOptionBinding import im.vector.app.databinding.ItemPollOptionBinding
@ -43,11 +44,12 @@ class PollOptionView @JvmOverloads constructor(
views.optionNameTextView.text = state.optionAnswer views.optionNameTextView.text = state.optionAnswer
when (state) { when (state) {
is PollOptionViewState.PollSending -> renderPollSending() is PollOptionViewState.PollSending -> renderPollSending()
is PollOptionViewState.PollEnded -> renderPollEnded(state) is PollOptionViewState.PollEnded -> renderPollEnded(state)
is PollOptionViewState.PollReady -> renderPollReady() is PollOptionViewState.PollReady -> renderPollReady()
is PollOptionViewState.PollVoted -> renderPollVoted(state) is PollOptionViewState.PollVoted -> renderPollVoted(state)
} is PollOptionViewState.PollUndisclosed -> renderPollUndisclosed(state)
}.exhaustive
} }
private fun renderPollSending() { private fun renderPollSending() {
@ -78,6 +80,12 @@ class PollOptionView @JvmOverloads constructor(
renderVoteSelection(state.isSelected) renderVoteSelection(state.isSelected)
} }
private fun renderPollUndisclosed(state: PollOptionViewState.PollUndisclosed) {
views.optionCheckImageView.isVisible = true
views.optionWinnerImageView.isVisible = false
renderVoteSelection(state.isSelected)
}
private fun showVotes(voteCount: Int, votePercentage: Double) { private fun showVotes(voteCount: Int, votePercentage: Double) {
views.optionVoteCountTextView.apply { views.optionVoteCountTextView.apply {
isVisible = true isVisible = true

View File

@ -51,4 +51,12 @@ sealed class PollOptionViewState(open val optionId: String,
val votePercentage: Double, val votePercentage: Double,
val isWinner: Boolean val isWinner: Boolean
) : PollOptionViewState(optionId, optionAnswer) ) : PollOptionViewState(optionId, optionAnswer)
/**
* Represent a poll that is closed, votes will be hidden until the poll is ended.
*/
data class PollUndisclosed(override val optionId: String,
override val optionAnswer: String,
val isSelected: Boolean
) : PollOptionViewState(optionId, optionAnswer)
} }

View File

@ -17,11 +17,13 @@
package im.vector.app.features.poll.create package im.vector.app.features.poll.create
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.model.message.PollType
sealed class CreatePollAction : VectorViewModelAction { sealed class CreatePollAction : VectorViewModelAction {
data class OnQuestionChanged(val question: String) : CreatePollAction() data class OnQuestionChanged(val question: String) : CreatePollAction()
data class OnOptionChanged(val index: Int, val option: String) : CreatePollAction() data class OnOptionChanged(val index: Int, val option: String) : CreatePollAction()
data class OnDeleteOption(val index: Int) : CreatePollAction() data class OnDeleteOption(val index: Int) : CreatePollAction()
data class OnPollTypeChanged(val pollType: PollType) : CreatePollAction()
object OnAddOption : CreatePollAction() object OnAddOption : CreatePollAction()
object OnCreatePoll : CreatePollAction() object OnCreatePoll : CreatePollAction()
} }

View File

@ -28,6 +28,7 @@ import im.vector.app.core.ui.list.genericItem
import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditTextItem
import im.vector.app.features.form.formEditTextWithDeleteItem import im.vector.app.features.form.formEditTextWithDeleteItem
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.session.room.model.message.PollType
import javax.inject.Inject import javax.inject.Inject
class CreatePollController @Inject constructor( class CreatePollController @Inject constructor(
@ -47,6 +48,26 @@ class CreatePollController @Inject constructor(
val currentState = state ?: return val currentState = state ?: return
val host = this val host = this
genericItem {
id("poll_type_title")
style(ItemStyle.BIG_TEXT)
title(host.stringProvider.getString(R.string.poll_type_title).toEpoxyCharSequence())
}
pollTypeSelectionItem {
id("poll_type_selection")
pollType(currentState.pollType)
pollTypeChangedListener { _, id ->
host.callback?.onPollTypeChanged(
if (id == R.id.openPollTypeRadioButton) {
PollType.DISCLOSED
} else {
PollType.UNDISCLOSED
}
)
}
}
genericItem { genericItem {
id("question_title") id("question_title")
style(ItemStyle.BIG_TEXT) style(ItemStyle.BIG_TEXT)
@ -110,5 +131,6 @@ class CreatePollController @Inject constructor(
fun onOptionChanged(index: Int, option: String) fun onOptionChanged(index: Int, option: String)
fun onDeleteOption(index: Int) fun onDeleteOption(index: Int)
fun onAddOption() fun onAddOption()
fun onPollTypeChanged(type: PollType)
} }
} }

View File

@ -32,6 +32,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentCreatePollBinding import im.vector.app.databinding.FragmentCreatePollBinding
import im.vector.app.features.poll.create.CreatePollViewModel.Companion.MAX_OPTIONS_COUNT import im.vector.app.features.poll.create.CreatePollViewModel.Companion.MAX_OPTIONS_COUNT
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.room.model.message.PollType
import javax.inject.Inject import javax.inject.Inject
@Parcelize @Parcelize
@ -60,18 +61,18 @@ class CreatePollFragment @Inject constructor(
when (args.mode) { when (args.mode) {
PollMode.CREATE -> { PollMode.CREATE -> {
views.createPollTitle.text = getString(R.string.create_poll_title) views.createPollToolbar.title = getString(R.string.create_poll_title)
views.createPollButton.text = getString(R.string.create_poll_title) views.createPollButton.text = getString(R.string.create_poll_title)
} }
PollMode.EDIT -> { PollMode.EDIT -> {
views.createPollTitle.text = getString(R.string.edit_poll_title) views.createPollToolbar.title = getString(R.string.edit_poll_title)
views.createPollButton.text = getString(R.string.edit_poll_title) views.createPollButton.text = getString(R.string.edit_poll_title)
} }
}.exhaustive }.exhaustive
views.createPollRecyclerView.configureWith(controller, disableItemAnimation = true) views.createPollRecyclerView.configureWith(controller, disableItemAnimation = true)
// workaround for https://github.com/vector-im/element-android/issues/4735 // workaround for https://github.com/vector-im/element-android/issues/4735
views.createPollRecyclerView.setItemViewCacheSize(MAX_OPTIONS_COUNT + 4) views.createPollRecyclerView.setItemViewCacheSize(MAX_OPTIONS_COUNT + 6)
controller.callback = this controller.callback = this
views.createPollButton.debouncedClicks { views.createPollButton.debouncedClicks {
@ -117,6 +118,10 @@ class CreatePollFragment @Inject constructor(
} }
} }
override fun onPollTypeChanged(type: PollType) {
viewModel.handle(CreatePollAction.OnPollTypeChanged(type))
}
private fun handleSuccess() { private fun handleSuccess() {
requireActivity().finish() requireActivity().finish()
} }

View File

@ -25,6 +25,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.PollType
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
class CreatePollViewModel @AssistedInject constructor( class CreatePollViewModel @AssistedInject constructor(
@ -70,13 +71,15 @@ class CreatePollViewModel @AssistedInject constructor(
val event = room.getTimeLineEvent(eventId) ?: return val event = room.getTimeLineEvent(eventId) ?: return
val content = event.getLastMessageContent() as? MessagePollContent ?: return val content = event.getLastMessageContent() as? MessagePollContent ?: return
val pollType = content.pollCreationInfo?.kind ?: PollType.DISCLOSED
val question = content.pollCreationInfo?.question?.question ?: "" val question = content.pollCreationInfo?.question?.question ?: ""
val options = content.pollCreationInfo?.answers?.mapNotNull { it.answer } ?: List(MIN_OPTIONS_COUNT) { "" } val options = content.pollCreationInfo?.answers?.mapNotNull { it.answer } ?: List(MIN_OPTIONS_COUNT) { "" }
setState { setState {
copy( copy(
question = question, question = question,
options = options options = options,
pollType = pollType
) )
} }
} }
@ -88,6 +91,7 @@ class CreatePollViewModel @AssistedInject constructor(
is CreatePollAction.OnDeleteOption -> handleOnDeleteOption(action.index) is CreatePollAction.OnDeleteOption -> handleOnDeleteOption(action.index)
is CreatePollAction.OnOptionChanged -> handleOnOptionChanged(action.index, action.option) is CreatePollAction.OnOptionChanged -> handleOnOptionChanged(action.index, action.option)
is CreatePollAction.OnQuestionChanged -> handleOnQuestionChanged(action.question) is CreatePollAction.OnQuestionChanged -> handleOnQuestionChanged(action.question)
is CreatePollAction.OnPollTypeChanged -> handleOnPollTypeChanged(action.pollType)
} }
} }
@ -102,17 +106,17 @@ class CreatePollViewModel @AssistedInject constructor(
} }
else -> { else -> {
when (state.mode) { when (state.mode) {
PollMode.CREATE -> room.sendPoll(state.question, nonEmptyOptions) PollMode.CREATE -> room.sendPoll(state.pollType, state.question, nonEmptyOptions)
PollMode.EDIT -> sendEditedPoll(state.editedEventId!!, state.question, nonEmptyOptions) PollMode.EDIT -> sendEditedPoll(state.editedEventId!!, state.pollType, state.question, nonEmptyOptions)
} }
_viewEvents.post(CreatePollViewEvents.Success) _viewEvents.post(CreatePollViewEvents.Success)
} }
} }
} }
private fun sendEditedPoll(editedEventId: String, question: String, options: List<String>) { private fun sendEditedPoll(editedEventId: String, pollType: PollType, question: String, options: List<String>) {
val editedEvent = room.getTimeLineEvent(editedEventId) ?: return val editedEvent = room.getTimeLineEvent(editedEventId) ?: return
room.editPoll(editedEvent, question, options) room.editPoll(editedEvent, pollType, question, options)
} }
private fun handleOnAddOption() { private fun handleOnAddOption() {
@ -150,6 +154,14 @@ class CreatePollViewModel @AssistedInject constructor(
} }
} }
private fun handleOnPollTypeChanged(pollType: PollType) {
setState {
copy(
pollType = pollType
)
}
}
private fun canCreatePoll(question: String, options: List<String>): Boolean { private fun canCreatePoll(question: String, options: List<String>): Boolean {
return question.isNotEmpty() && return question.isNotEmpty() &&
options.filter { it.isNotEmpty() }.size >= MIN_OPTIONS_COUNT options.filter { it.isNotEmpty() }.size >= MIN_OPTIONS_COUNT

View File

@ -17,6 +17,7 @@
package im.vector.app.features.poll.create package im.vector.app.features.poll.create
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import org.matrix.android.sdk.api.session.room.model.message.PollType
data class CreatePollViewState( data class CreatePollViewState(
val roomId: String, val roomId: String,
@ -25,7 +26,8 @@ data class CreatePollViewState(
val question: String = "", val question: String = "",
val options: List<String> = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" }, val options: List<String> = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" },
val canCreatePoll: Boolean = false, val canCreatePoll: Boolean = false,
val canAddMoreOptions: Boolean = true val canAddMoreOptions: Boolean = true,
val pollType: PollType = PollType.DISCLOSED
) : MavericksState { ) : MavericksState {
constructor(args: CreatePollArgs) : this( constructor(args: CreatePollArgs) : this(

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) 2022 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.poll.create
import android.widget.RadioGroup
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import org.matrix.android.sdk.api.session.room.model.message.PollType
@EpoxyModelClass(layout = R.layout.item_poll_type_selection)
abstract class PollTypeSelectionItem : VectorEpoxyModel<PollTypeSelectionItem.Holder>() {
@EpoxyAttribute
var pollType: PollType = PollType.DISCLOSED
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var pollTypeChangedListener: RadioGroup.OnCheckedChangeListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.pollTypeRadioGroup.check(
when (pollType) {
PollType.DISCLOSED -> R.id.openPollTypeRadioButton
PollType.UNDISCLOSED -> R.id.closedPollTypeRadioButton
}
)
holder.pollTypeRadioGroup.setOnCheckedChangeListener(pollTypeChangedListener)
}
override fun unbind(holder: Holder) {
super.unbind(holder)
holder.pollTypeRadioGroup.setOnCheckedChangeListener(null)
}
class Holder : VectorEpoxyHolder() {
val pollTypeRadioGroup by bind<RadioGroup>(R.id.pollTypeRadioGroup)
}
}

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pollTypeRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:orientation="vertical"
android:paddingBottom="@dimen/layout_vertical_margin">
<RadioButton
android:id="@+id/openPollTypeRadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="0dp"
android:text="@string/open_poll_option_title" />
<TextView
style="@style/TextAppearance.Vector.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:text="@string/open_poll_option_description" />
<RadioButton
android:id="@+id/closedPollTypeRadioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:minHeight="0dp"
android:text="@string/closed_poll_option_title" />
<TextView
style="@style/TextAppearance.Vector.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:text="@string/closed_poll_option_description" />
</RadioGroup>

View File

@ -3708,12 +3708,17 @@
<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_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 cast</string>
<string name="poll_end_room_list_preview">Poll ended</string> <string name="poll_end_room_list_preview">Poll ended</string>
<string name="delete_poll_dialog_title">Remove poll</string> <string name="delete_poll_dialog_title">Remove poll</string>
<string name="delete_poll_dialog_content">Are you sure you want to remove this poll? You won\'t be able to recover it once removed.</string> <string name="delete_poll_dialog_content">Are you sure you want to remove this poll? You won\'t be able to recover it once removed.</string>
<string name="edit_poll_title">Edit poll</string> <string name="edit_poll_title">Edit poll</string>
<string name="edit_poll_button">EDIT POLL</string> <string name="edit_poll_button">EDIT POLL</string>
<string name="poll_type_title">Poll type</string>
<string name="open_poll_option_title">Open poll</string>
<string name="open_poll_option_description">Voters see results as soon as they have voted</string>
<string name="closed_poll_option_title">Closed poll</string>
<string name="closed_poll_option_description">Results are only revealed when you end the poll</string>
<string name="tooltip_attachment_photo">Open camera</string> <string name="tooltip_attachment_photo">Open camera</string>
<string name="tooltip_attachment_gallery">Send images and videos</string> <string name="tooltip_attachment_gallery">Send images and videos</string>