Code review fixes.

This commit is contained in:
Onuray Sahin 2021-12-10 17:57:57 +03:00
parent be9e592aa5
commit 9b2a3cf445
11 changed files with 158 additions and 83 deletions

View File

@ -18,13 +18,12 @@ package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
/** /**
* Class representing the org.matrix.msc3381.poll.end event content * Class representing the org.matrix.msc3381.poll.end event content
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessageEndPollContent( data class MessageEndPollContent(
@Json(name = "rel_type") val relationType: String = RelationType.REFERENCE, @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null
@Json(name = "event_id") val eventId: String
) )

View File

@ -406,7 +406,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
content: MessageEndPollContent, content: MessageEndPollContent,
roomId: String, roomId: String,
isLocalEcho: Boolean) { isLocalEcho: Boolean) {
val pollEventId = content.eventId val pollEventId = content.relatesTo?.eventId ?: return
var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst() var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
if (existing == null) { if (existing == null) {

View File

@ -178,7 +178,10 @@ internal class LocalEchoEventFactory @Inject constructor(
fun createEndPollEvent(roomId: String, fun createEndPollEvent(roomId: String,
eventId: String): Event { eventId: String): Event {
val content = MessageEndPollContent( val content = MessageEndPollContent(
eventId = eventId relatesTo = RelationDefaultContent(
type = RelationType.REFERENCE,
eventId = eventId
)
) )
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
return Event( return Event(
@ -440,7 +443,7 @@ internal class LocalEchoEventFactory @Inject constructor(
when (content?.msgType) { when (content?.msgType) {
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE -> { MessageType.MSGTYPE_NOTICE -> {
var formattedText: String? = null var formattedText: String? = null
if (content is MessageContentWithFormattedBody) { if (content is MessageContentWithFormattedBody) {
formattedText = content.matrixFormattedBody formattedText = content.matrixFormattedBody
@ -451,11 +454,12 @@ internal class LocalEchoEventFactory @Inject constructor(
TextContent(content.body, formattedText) TextContent(content.body, formattedText)
} }
} }
MessageType.MSGTYPE_FILE -> return TextContent("sent a file.") MessageType.MSGTYPE_FILE -> return TextContent("sent a file.")
MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.") MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.") MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.") MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
else -> return TextContent(content?.body ?: "") MessageType.MSGTYPE_POLL_START -> return TextContent((content as? MessagePollContent)?.pollCreationInfo?.question?.question ?: "")
else -> return TextContent(content?.body ?: "")
} }
} }

View File

@ -203,6 +203,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFormat import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
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.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
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.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
@ -1077,6 +1078,8 @@ class RoomDetailFragment @Inject constructor(
val nonFormattedBody = if (messageContent is MessageAudioContent && messageContent.voiceMessageIndicator != null) { val nonFormattedBody = if (messageContent is MessageAudioContent && messageContent.voiceMessageIndicator != null) {
val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong()) val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong())
getString(R.string.voice_message_reply_content, formattedDuration) getString(R.string.voice_message_reply_content, formattedDuration)
} else if (messageContent is MessagePollContent) {
messageContent.pollCreationInfo?.question?.question
} else { } else {
messageContent?.body ?: "" messageContent?.body ?: ""
} }

View File

@ -399,8 +399,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean {
// Only event of type EventType.MESSAGE are supported for the moment // Only EventType.MESSAGE and EventType.POLL_START event types are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return false
if (!actionPermissions.canSendMessage) return false if (!actionPermissions.canSendMessage) return false
return when (messageContent?.msgType) { return when (messageContent?.msgType) {
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
@ -409,8 +409,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_AUDIO, MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE -> true MessageType.MSGTYPE_FILE,
else -> false MessageType.MSGTYPE_POLL_START -> true
else -> false
} }
} }

View File

@ -179,6 +179,7 @@ class MessageItemFactory @Inject constructor(
.attributes(attributes) .attributes(attributes)
.eventId(informationData.eventId) .eventId(informationData.eventId)
.pollResponseSummary(informationData.pollResponseAggregatedSummary) .pollResponseSummary(informationData.pollResponseAggregatedSummary)
.pollSent(informationData.sendState.isSent())
.pollContent(messageContent) .pollContent(messageContent)
.highlighted(highlight) .highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)

View File

@ -1,17 +0,0 @@
/*
* Copyright 2019 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

View File

@ -41,6 +41,9 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var eventId: String? = null var eventId: String? = null
@EpoxyAttribute
var pollSent: Boolean = false
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
val relatedEventId = eventId ?: return val relatedEventId = eventId ?: return
@ -53,7 +56,6 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
val isEnded = pollResponseSummary?.isClosed.orFalse() val isEnded = pollResponseSummary?.isClosed.orFalse()
val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse() val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse()
val showVotes = didUserVoted || isEnded
val totalVotes = pollResponseSummary?.totalVotes ?: 0 val totalVotes = pollResponseSummary?.totalVotes ?: 0
val winnerVoteCount = pollResponseSummary?.winnerVoteCount val winnerVoteCount = pollResponseSummary?.winnerVoteCount
@ -62,21 +64,30 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
val isMyVote = pollResponseSummary?.myVote == option.id val isMyVote = pollResponseSummary?.myVote == option.id
val voteCount = voteSummary?.total ?: 0 val voteCount = voteSummary?.total ?: 0
val votePercentage = voteSummary?.percentage ?: 0.0 val votePercentage = voteSummary?.percentage ?: 0.0
val optionName = option.answer ?: ""
holder.optionsContainer.addView( holder.optionsContainer.addView(
PollOptionItem(holder.view.context).apply { PollOptionItem(holder.view.context).apply {
update(optionName = option.answer ?: "", val callback = object : PollOptionItem.Callback {
isSelected = isMyVote, override fun onOptionClicked() {
isWinner = voteCount == winnerVoteCount, callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, option.id ?: ""))
isEnded = isEnded, }
showVote = showVotes, }
voteCount = voteCount,
votePercentage = votePercentage, if (!pollSent) {
callback = object : PollOptionItem.Callback { // Poll event is not send yet. Disable option.
override fun onOptionClicked() { render(PollOptionViewState.DisabledOptionWithInvisibleVotes(optionName), callback)
callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, option.id ?: "")) } else if (isEnded) {
} // Poll is ended. Disable option, show votes and mark the winner.
}) val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount
render(PollOptionViewState.DisabledOptionWithVisibleVotes(optionName, voteCount, votePercentage, isWinner), callback)
} else if (didUserVoted) {
// User voted to the poll, but poll is not ended. Enable option, show votes and mark the user's selection.
render(PollOptionViewState.EnabledOptionWithVisibleVotes(optionName, voteCount, votePercentage, isMyVote), callback)
} else {
// User didn't voted yet and poll is not ended yet. Enable options, hide votes.
render(PollOptionViewState.EnabledOptionWithInvisibleVotes(optionName), callback)
}
} }
) )
} }

View File

@ -50,51 +50,76 @@ class PollOptionItem @JvmOverloads constructor(
views.root.setOnClickListener { callback?.onOptionClicked() } views.root.setOnClickListener { callback?.onOptionClicked() }
} }
fun update(optionName: String, fun render(state: PollOptionViewState, callback: Callback) {
isSelected: Boolean,
isWinner: Boolean,
isEnded: Boolean,
showVote: Boolean,
voteCount: Int,
votePercentage: Double,
callback: Callback) {
this.callback = callback this.callback = callback
views.optionNameTextView.text = optionName
views.optionCheckImageView.isVisible = !isEnded views.optionNameTextView.text = state.name
if (isEnded && isWinner) { when (state) {
views.optionBorderImageView.setAttributeTintedImageResource(R.drawable.bg_poll_option, R.attr.colorPrimary) is PollOptionViewState.DisabledOptionWithInvisibleVotes -> renderDisabledOptionWithInvisibleVotes()
views.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_checked) is PollOptionViewState.DisabledOptionWithVisibleVotes -> renderDisabledOptionWithVisibleVotes(state)
views.optionWinnerImageView.isVisible = true is PollOptionViewState.EnabledOptionWithInvisibleVotes -> renderEnabledOptionWithInvisibleVotes()
} else if (isSelected) { is PollOptionViewState.EnabledOptionWithVisibleVotes -> renderEnabledOptionWithVisibleVotes(state)
}
}
private fun renderDisabledOptionWithInvisibleVotes() {
views.optionCheckImageView.isVisible = false
views.optionWinnerImageView.isVisible = false
hideVotes()
renderVoteSelection(false)
}
private fun renderDisabledOptionWithVisibleVotes(state: PollOptionViewState.DisabledOptionWithVisibleVotes) {
views.optionCheckImageView.isVisible = false
views.optionWinnerImageView.isVisible = state.isWinner
showVotes(state.voteCount, state.votePercentage)
renderVoteSelection(state.isWinner)
}
private fun renderEnabledOptionWithInvisibleVotes() {
views.optionCheckImageView.isVisible = true
views.optionWinnerImageView.isVisible = false
hideVotes()
renderVoteSelection(false)
}
private fun renderEnabledOptionWithVisibleVotes(state: PollOptionViewState.EnabledOptionWithVisibleVotes) {
views.optionCheckImageView.isVisible = true
views.optionWinnerImageView.isVisible = false
showVotes(state.voteCount, state.votePercentage)
renderVoteSelection(state.isSelected)
}
private fun showVotes(voteCount: Int, votePercentage: Double) {
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
}
}
}
private fun hideVotes() {
views.optionVoteCountTextView.isVisible = false
views.optionVoteProgress.progress = 0
}
private fun renderVoteSelection(isSelected: Boolean) {
if (isSelected) {
views.optionBorderImageView.setAttributeTintedImageResource(R.drawable.bg_poll_option, R.attr.colorPrimary) views.optionBorderImageView.setAttributeTintedImageResource(R.drawable.bg_poll_option, R.attr.colorPrimary)
views.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_checked) views.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_checked)
views.optionCheckImageView.setImageResource(R.drawable.poll_option_checked) views.optionCheckImageView.setImageResource(R.drawable.poll_option_checked)
views.optionWinnerImageView.isVisible = false
} else { } else {
views.optionBorderImageView.setAttributeTintedImageResource(R.drawable.bg_poll_option, R.attr.vctr_content_quinary) 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.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_unchecked)
views.optionCheckImageView.setImageResource(R.drawable.poll_option_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,48 @@
/*
* 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
sealed class PollOptionViewState(open val name: String) {
/**
* Represents a poll that user already voted.
*/
data class EnabledOptionWithVisibleVotes(override val name: String,
val voteCount: Int,
val votePercentage: Double,
val isSelected: Boolean
) : PollOptionViewState(name)
/**
* Represents a poll that is ended.
*/
data class DisabledOptionWithVisibleVotes(override val name: String,
val voteCount: Int,
val votePercentage: Double,
val isWinner: Boolean
) : PollOptionViewState(name)
/**
* Represents a poll that is sent but not voted by the user
*/
data class EnabledOptionWithInvisibleVotes(override val name: String) : PollOptionViewState(name)
/**
* Represents a poll that is not sent to the server yet.
*/
data class DisabledOptionWithInvisibleVotes(override val name: String) : PollOptionViewState(name)
}

View File

@ -7,10 +7,10 @@
<TextView <TextView
android:id="@+id/questionTextView" android:id="@+id/questionTextView"
style="@style/Widget.Vector.TextView.HeadlineMedium" style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="8dp"
android:textColor="?vctr_content_primary" android:textColor="?vctr_content_primary"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -22,7 +22,7 @@
android:id="@+id/optionsContainer" android:id="@+id/optionsContainer"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="12dp"
android:divider="@drawable/divider_poll_options" android:divider="@drawable/divider_poll_options"
android:orientation="vertical" android:orientation="vertical"
android:showDividers="middle" android:showDividers="middle"