From 9b2a3cf445b950c3841ead2d1cf592b2a8a05997 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 10 Dec 2021 17:57:57 +0300 Subject: [PATCH] Code review fixes. --- .../model/message/MessageEndPollContent.kt | 5 +- .../EventRelationsAggregationProcessor.kt | 2 +- .../room/send/LocalEchoEventFactory.kt | 18 ++-- .../home/room/detail/RoomDetailFragment.kt | 3 + .../action/MessageActionsViewModel.kt | 9 +- .../timeline/factory/MessageItemFactory.kt | 1 + .../timeline/item/EventItemAttributes.kt | 17 ---- .../room/detail/timeline/item/PollItem.kt | 37 +++++--- .../detail/timeline/item/PollOptionItem.kt | 95 ++++++++++++------- .../timeline/item/PollOptionViewState.kt | 48 ++++++++++ .../res/layout/item_timeline_event_poll.xml | 6 +- 11 files changed, 158 insertions(+), 83 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/EventItemAttributes.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt index e64705e868..491b71477e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageEndPollContent.kt @@ -18,13 +18,12 @@ package org.matrix.android.sdk.api.session.room.model.message import com.squareup.moshi.Json 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 */ @JsonClass(generateAdapter = true) data class MessageEndPollContent( - @Json(name = "rel_type") val relationType: String = RelationType.REFERENCE, - @Json(name = "event_id") val eventId: String + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 66fb0362e3..62b6d626f5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -406,7 +406,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( content: MessageEndPollContent, roomId: String, isLocalEcho: Boolean) { - val pollEventId = content.eventId + val pollEventId = content.relatesTo?.eventId ?: return var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst() if (existing == null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index e98c4925fe..85b22628d7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -178,7 +178,10 @@ internal class LocalEchoEventFactory @Inject constructor( fun createEndPollEvent(roomId: String, eventId: String): Event { val content = MessageEndPollContent( - eventId = eventId + relatesTo = RelationDefaultContent( + type = RelationType.REFERENCE, + eventId = eventId + ) ) val localId = LocalEcho.createLocalEchoId() return Event( @@ -440,7 +443,7 @@ internal class LocalEchoEventFactory @Inject constructor( when (content?.msgType) { MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_TEXT, - MessageType.MSGTYPE_NOTICE -> { + MessageType.MSGTYPE_NOTICE -> { var formattedText: String? = null if (content is MessageContentWithFormattedBody) { formattedText = content.matrixFormattedBody @@ -451,11 +454,12 @@ internal class LocalEchoEventFactory @Inject constructor( TextContent(content.body, formattedText) } } - MessageType.MSGTYPE_FILE -> return TextContent("sent a file.") - MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.") - MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.") - MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.") - else -> return TextContent(content?.body ?: "") + MessageType.MSGTYPE_FILE -> return TextContent("sent a file.") + MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.") + MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.") + MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.") + MessageType.MSGTYPE_POLL_START -> return TextContent((content as? MessagePollContent)?.pollCreationInfo?.question?.question ?: "") + else -> return TextContent(content?.body ?: "") } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index f4ff94476c..4848ef8b38 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -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.MessageFormat 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.MessageTextContent 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 formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong()) getString(R.string.voice_message_reply_content, formattedDuration) + } else if (messageContent is MessagePollContent) { + messageContent.pollCreationInfo?.question?.question } else { messageContent?.body ?: "" } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 60ef9874c0..d61e961936 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -399,8 +399,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { - // Only event of type EventType.MESSAGE are supported for the moment - if (event.root.getClearType() != EventType.MESSAGE) return false + // Only EventType.MESSAGE and EventType.POLL_START event types are supported for the moment + if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return false if (!actionPermissions.canSendMessage) return false return when (messageContent?.msgType) { MessageType.MSGTYPE_TEXT, @@ -409,8 +409,9 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_AUDIO, - MessageType.MSGTYPE_FILE -> true - else -> false + MessageType.MSGTYPE_FILE, + MessageType.MSGTYPE_POLL_START -> true + else -> false } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 7f4a9b6674..43cd1d4a41 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -179,6 +179,7 @@ class MessageItemFactory @Inject constructor( .attributes(attributes) .eventId(informationData.eventId) .pollResponseSummary(informationData.pollResponseAggregatedSummary) + .pollSent(informationData.sendState.isSent()) .pollContent(messageContent) .highlighted(highlight) .leftGuideline(avatarSizeProvider.leftGuideline) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/EventItemAttributes.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/EventItemAttributes.kt deleted file mode 100644 index 0762c4952f..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/EventItemAttributes.kt +++ /dev/null @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 80cb79ac77..aa527d30de 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -41,6 +41,9 @@ abstract class PollItem : AbsMessageItem() { @EpoxyAttribute var eventId: String? = null + @EpoxyAttribute + var pollSent: Boolean = false + override fun bind(holder: Holder) { super.bind(holder) val relatedEventId = eventId ?: return @@ -53,7 +56,6 @@ abstract class PollItem : AbsMessageItem() { val isEnded = pollResponseSummary?.isClosed.orFalse() val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse() - val showVotes = didUserVoted || isEnded val totalVotes = pollResponseSummary?.totalVotes ?: 0 val winnerVoteCount = pollResponseSummary?.winnerVoteCount @@ -62,21 +64,30 @@ abstract class PollItem : AbsMessageItem() { val isMyVote = pollResponseSummary?.myVote == option.id val voteCount = voteSummary?.total ?: 0 val votePercentage = voteSummary?.percentage ?: 0.0 + val optionName = option.answer ?: "" holder.optionsContainer.addView( PollOptionItem(holder.view.context).apply { - update(optionName = option.answer ?: "", - isSelected = isMyVote, - isWinner = voteCount == winnerVoteCount, - isEnded = isEnded, - showVote = showVotes, - voteCount = voteCount, - votePercentage = votePercentage, - callback = object : PollOptionItem.Callback { - override fun onOptionClicked() { - callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, option.id ?: "")) - } - }) + val callback = object : PollOptionItem.Callback { + override fun onOptionClicked() { + callback?.onTimelineItemAction(RoomDetailAction.VoteToPoll(relatedEventId, option.id ?: "")) + } + } + + if (!pollSent) { + // Poll event is not send yet. Disable option. + render(PollOptionViewState.DisabledOptionWithInvisibleVotes(optionName), callback) + } 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) + } } ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionItem.kt index aed1210532..8693f155f4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionItem.kt @@ -50,51 +50,76 @@ class PollOptionItem @JvmOverloads constructor( views.root.setOnClickListener { callback?.onOptionClicked() } } - fun update(optionName: String, - isSelected: Boolean, - isWinner: Boolean, - isEnded: Boolean, - showVote: Boolean, - voteCount: Int, - votePercentage: Double, - callback: Callback) { + fun render(state: PollOptionViewState, callback: Callback) { this.callback = callback - views.optionNameTextView.text = optionName - views.optionCheckImageView.isVisible = !isEnded + views.optionNameTextView.text = state.name - if (isEnded && isWinner) { - views.optionBorderImageView.setAttributeTintedImageResource(R.drawable.bg_poll_option, R.attr.colorPrimary) - views.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_checked) - views.optionWinnerImageView.isVisible = true - } else if (isSelected) { + when (state) { + is PollOptionViewState.DisabledOptionWithInvisibleVotes -> renderDisabledOptionWithInvisibleVotes() + is PollOptionViewState.DisabledOptionWithVisibleVotes -> renderDisabledOptionWithVisibleVotes(state) + is PollOptionViewState.EnabledOptionWithInvisibleVotes -> renderEnabledOptionWithInvisibleVotes() + 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.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_checked) views.optionCheckImageView.setImageResource(R.drawable.poll_option_checked) - views.optionWinnerImageView.isVisible = false } else { views.optionBorderImageView.setAttributeTintedImageResource(R.drawable.bg_poll_option, R.attr.vctr_content_quinary) views.optionVoteProgress.progressDrawable = AppCompatResources.getDrawable(context, R.drawable.poll_option_progressbar_unchecked) views.optionCheckImageView.setImageResource(R.drawable.poll_option_unchecked) - views.optionWinnerImageView.isVisible = false - } - - if (showVote) { - views.optionVoteCountTextView.apply { - isVisible = true - text = resources.getQuantityString(R.plurals.poll_option_vote_count, voteCount, voteCount) - } - views.optionVoteProgress.apply { - val progressValue = (votePercentage * 100).toInt() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - setProgress(progressValue, true) - } else { - progress = progressValue - } - } - } else { - views.optionVoteCountTextView.isVisible = false - views.optionVoteProgress.progress = 0 } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt new file mode 100644 index 0000000000..72dda3df52 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollOptionViewState.kt @@ -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) +} diff --git a/vector/src/main/res/layout/item_timeline_event_poll.xml b/vector/src/main/res/layout/item_timeline_event_poll.xml index 0b84336b3d..575b2b4d4e 100644 --- a/vector/src/main/res/layout/item_timeline_event_poll.xml +++ b/vector/src/main/res/layout/item_timeline_event_poll.xml @@ -7,10 +7,10 @@