Code review fixes.

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

View File

@ -27,17 +27,17 @@ data class PollSummaryContent(
var myVote: String? = null,
// 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(

View File

@ -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,

View File

@ -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,

View File

@ -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"
}

View File

@ -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.

View File

@ -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)) {

View File

@ -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) }
}

View File

@ -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)
)
)

View File

@ -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
}
}

View File

@ -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,

View File

@ -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) {

View File

@ -23,6 +23,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFact
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.item.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(),

View File

@ -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,

View File

@ -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 ?: ""))
}
})
}

View File

@ -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"

View File

@ -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>