Code review fixes.
This commit is contained in:
parent
0f11e498a0
commit
04a7590804
|
@ -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<VoteInfo>? = null
|
||||
) {
|
||||
var votes: List<VoteInfo>? = null,
|
||||
var votesSummary: Map<String, VoteSummary>? = 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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
} else if (event.getClearType() == EventType.POLL_RESPONSE) {
|
||||
event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { pollResponseContent ->
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.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<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
|
||||
// is it a remote echo?
|
||||
if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -72,10 +72,18 @@ data class ReadReceiptData(
|
|||
@Parcelize
|
||||
data class PollResponseData(
|
||||
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
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class PollVoteSummaryData(
|
||||
val total: Int = 0,
|
||||
val percentage: Double = 0.0
|
||||
) : Parcelable
|
||||
|
||||
enum class E2EDecoration {
|
||||
NONE,
|
||||
WARN_IN_CLEAR,
|
||||
|
|
|
@ -54,13 +54,14 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
|
|||
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<PollItem.Holder>() {
|
|||
votePercentage = votePercentage,
|
||||
callback = object : PollOptionItem.Callback {
|
||||
override fun onOptionClicked() {
|
||||
callback?.onTimelineItemAction(RoomDetailAction.RegisterVoteToPoll(relatedEventId, option.id ?: ""))
|
||||
callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, option.id ?: ""))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/optionCheckImageView"
|
||||
|
@ -23,10 +23,10 @@
|
|||
android:layout_height="20dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/poll_option_unchecked"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/optionNameTextView"
|
||||
|
|
|
@ -3654,26 +3654,26 @@
|
|||
<item quantity="other">At least %1$s options are required</item>
|
||||
</plurals>
|
||||
<plurals name="poll_option_vote_count">
|
||||
<item quantity="one">%1$s vote</item>
|
||||
<item quantity="other">%1$s votes</item>
|
||||
<item quantity="one">%1$d vote</item>
|
||||
<item quantity="other">%1$d 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>
|
||||
<item quantity="one">Based on %1$d vote</item>
|
||||
<item quantity="other">Based on %1$d 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>
|
||||
<item quantity="one">%1$d vote cast. Vote to the see the results</item>
|
||||
<item quantity="other">%1$d 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>
|
||||
<item quantity="one">Final result based on %1$d vote</item>
|
||||
<item quantity="other">Final result based on %1$d votes</item>
|
||||
</plurals>
|
||||
<string name="poll_end_action">End poll</string>
|
||||
<string name="a11y_poll_winner_option">winner option</string>
|
||||
<string name="end_poll_confirmation_title">End 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_title">End this 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="labs_enable_polls">Enable Polls</string>
|
||||
<string name="poll_response_room_list_preview">Vote casted</string>
|
||||
|
|
Loading…
Reference in New Issue