From bd9fa483127b3acacee8cf1ecb010e5fdb5875a9 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 21 Jun 2022 17:03:56 +0300 Subject: [PATCH 01/24] Refactor poll item factory to make it testable. --- .../timeline/factory/MessageItemFactory.kt | 161 +----------------- .../factory/MessageItemFactoryHelper.kt | 82 +++++++++ .../timeline/factory/PollItemFactory.kt | 136 +++++++++++++++ 3 files changed, 223 insertions(+), 156 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt 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 54bfbdd8a0..fba6ffbe51 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 @@ -16,13 +16,8 @@ package im.vector.app.features.home.room.detail.timeline.factory -import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned -import android.text.TextPaint -import android.text.style.AbsoluteSizeSpan -import android.text.style.ClickableSpan -import android.text.style.ForegroundColorSpan import android.view.View import dagger.Lazy import im.vector.app.R @@ -35,6 +30,7 @@ import im.vector.app.core.time.Clock import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.containsOnlyEmojis import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.factory.MessageItemFactoryHelper.annotateWithEdited import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder @@ -57,14 +53,6 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_ -import im.vector.app.features.home.room.detail.timeline.item.PollItem -import im.vector.app.features.home.room.detail.timeline.item.PollItem_ -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollEnded -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollReady -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollSending -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollUndisclosed -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollVoted -import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem @@ -81,18 +69,11 @@ import im.vector.app.features.location.UrlMapProvider import im.vector.app.features.location.toLocationData import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer -import im.vector.app.features.poll.PollState -import im.vector.app.features.poll.PollState.Ended -import im.vector.app.features.poll.PollState.Ready -import im.vector.app.features.poll.PollState.Sending -import im.vector.app.features.poll.PollState.Undisclosed -import im.vector.app.features.poll.PollState.Voted import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.voice.AudioWaveformView import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.api.session.events.model.RelationType @@ -113,8 +94,6 @@ 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.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent -import org.matrix.android.sdk.api.session.room.model.message.PollAnswer -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.getThumbnailUrl import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent @@ -149,6 +128,7 @@ class MessageItemFactory @Inject constructor( private val vectorPreferences: VectorPreferences, private val urlMapProvider: UrlMapProvider, private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory, + private val pollItemFactory: PollItemFactory, ) { // TODO inject this properly? @@ -208,7 +188,7 @@ class MessageItemFactory @Inject constructor( is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) + is MessagePollContent -> pollItemFactory.create(messageContent, informationData, highlight, callback, attributes) is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes) is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) @@ -244,93 +224,6 @@ class MessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) } - private fun buildPollItem( - pollContent: MessagePollContent, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes, - ): PollItem { - val pollResponseSummary = informationData.pollResponseAggregatedSummary - val pollState = createPollState(informationData, pollResponseSummary, pollContent) - val pollCreationInfo = pollContent.getBestPollCreationInfo() - val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty() - val question = createPollQuestion(informationData, questionText, callback) - val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData) - val totalVotesText = createTotalVotesText(pollState, pollResponseSummary) - - return PollItem_() - .attributes(attributes) - .eventId(informationData.eventId) - .pollQuestion(question) - .canVote(pollState.isVotable()) - .totalVotesText(totalVotesText) - .optionViewStates(optionViewStates) - .edited(informationData.hasBeenEdited) - .highlighted(highlight) - .leftGuideline(avatarSizeProvider.leftGuideline) - .callback(callback) - } - - private fun createPollState( - informationData: MessageInformationData, - pollResponseSummary: PollResponseData?, - pollContent: MessagePollContent, - ): PollState = when { - !informationData.sendState.isSent() -> Sending - pollResponseSummary?.isClosed.orFalse() -> Ended - pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> Undisclosed - pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> Voted(pollResponseSummary?.totalVotes ?: 0) - else -> Ready - } - - private fun List.mapToOptions( - pollState: PollState, - informationData: MessageInformationData, - ) = map { answer -> - val pollResponseSummary = informationData.pollResponseAggregatedSummary - val winnerVoteCount = pollResponseSummary?.winnerVoteCount - val optionId = answer.id ?: "" - val optionAnswer = answer.getBestAnswer() ?: "" - val voteSummary = pollResponseSummary?.votes?.get(answer.id) - val voteCount = voteSummary?.total ?: 0 - val votePercentage = voteSummary?.percentage ?: 0.0 - val isMyVote = pollResponseSummary?.myVote == answer.id - val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount - - when (pollState) { - Sending -> PollSending(optionId, optionAnswer) - Ready -> PollReady(optionId, optionAnswer) - is Voted -> PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote) - Undisclosed -> PollUndisclosed(optionId, optionAnswer, isMyVote) - Ended -> PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner) - } - } - - private fun createPollQuestion( - informationData: MessageInformationData, - question: String, - callback: TimelineEventController.Callback?, - ) = if (informationData.hasBeenEdited) { - annotateWithEdited(question, callback, informationData) - } else { - question - }.toEpoxyCharSequence() - - private fun createTotalVotesText( - pollState: PollState, - pollResponseSummary: PollResponseData?, - ): String { - val votes = pollResponseSummary?.totalVotes ?: 0 - return when { - pollState is Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes) - pollState is Undisclosed -> "" - pollState is Voted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, votes, votes) - votes == 0 -> stringProvider.getString(R.string.poll_no_votes_cast) - else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes) - } - } - private fun buildAudioMessageItem( params: TimelineItemFactoryParams, messageContent: MessageAudioContent, @@ -627,7 +520,7 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .message( if (informationData.hasBeenEdited) { - annotateWithEdited(linkifiedBody, callback, informationData) + annotateWithEdited(stringProvider, colorProvider, dimensionConverter, linkifiedBody, callback, informationData) } else { linkifiedBody }.toEpoxyCharSequence() @@ -645,50 +538,6 @@ class MessageItemFactory @Inject constructor( .movementMethod(createLinkMovementMethod(callback)) } - private fun annotateWithEdited( - linkifiedBody: CharSequence, - callback: TimelineEventController.Callback?, - informationData: MessageInformationData, - ): Spannable { - val spannable = SpannableStringBuilder() - spannable.append(linkifiedBody) - val editedSuffix = stringProvider.getString(R.string.edited_suffix) - spannable.append(" ").append(editedSuffix) - val color = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) - val editStart = spannable.lastIndexOf(editedSuffix) - val editEnd = editStart + editedSuffix.length - spannable.setSpan( - ForegroundColorSpan(color), - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - - // Note: text size is set to 14sp - spannable.setSpan( - AbsoluteSizeSpan(dimensionConverter.spToPx(13)), - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - - spannable.setSpan( - object : ClickableSpan() { - override fun onClick(widget: View) { - callback?.onEditedDecorationClicked(informationData) - } - - override fun updateDrawState(ds: TextPaint) { - // nop - } - }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - return spannable - } - private fun buildNoticeMessageItem( messageContent: MessageNoticeContent, @Suppress("UNUSED_PARAMETER") @@ -735,7 +584,7 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .message( if (informationData.hasBeenEdited) { - annotateWithEdited(message, callback, informationData) + annotateWithEdited(stringProvider, colorProvider, dimensionConverter, message, callback, informationData) } else { message }.toEpoxyCharSequence() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt new file mode 100644 index 0000000000..0c4c7238e7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt @@ -0,0 +1,82 @@ +/* + * 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.home.room.detail.timeline.factory + +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextPaint +import android.text.style.AbsoluteSizeSpan +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.view.View +import im.vector.app.R +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData + +object MessageItemFactoryHelper { + + fun annotateWithEdited( + stringProvider: StringProvider, + colorProvider: ColorProvider, + dimensionConverter: DimensionConverter, + linkifiedBody: CharSequence, + callback: TimelineEventController.Callback?, + informationData: MessageInformationData, + ): Spannable { + val spannable = SpannableStringBuilder() + spannable.append(linkifiedBody) + val editedSuffix = stringProvider.getString(R.string.edited_suffix) + spannable.append(" ").append(editedSuffix) + val color = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) + val editStart = spannable.lastIndexOf(editedSuffix) + val editEnd = editStart + editedSuffix.length + spannable.setSpan( + ForegroundColorSpan(color), + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + + // Note: text size is set to 14sp + spannable.setSpan( + AbsoluteSizeSpan(dimensionConverter.spToPx(13)), + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + + spannable.setSpan( + object : ClickableSpan() { + override fun onClick(widget: View) { + callback?.onEditedDecorationClicked(informationData) + } + + override fun updateDrawState(ds: TextPaint) { + // nop + } + }, + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + return spannable + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt new file mode 100644 index 0000000000..09b601f080 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt @@ -0,0 +1,136 @@ +/* + * 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.home.room.detail.timeline.factory + +import androidx.annotation.VisibleForTesting +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.factory.MessageItemFactoryHelper.annotateWithEdited +import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.PollItem_ +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData +import im.vector.app.features.poll.PollState +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollAnswer +import org.matrix.android.sdk.api.session.room.model.message.PollType +import javax.inject.Inject + +class PollItemFactory @Inject constructor( + private val stringProvider: StringProvider, + private val avatarSizeProvider: AvatarSizeProvider, + private val colorProvider: ColorProvider, + private val dimensionConverter: DimensionConverter, +) { + + fun create( + pollContent: MessagePollContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): VectorEpoxyModel<*>? { + val pollResponseSummary = informationData.pollResponseAggregatedSummary + val pollState = createPollState(informationData, pollResponseSummary, pollContent) + val pollCreationInfo = pollContent.getBestPollCreationInfo() + val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty() + val question = createPollQuestion(informationData, questionText, callback) + val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData) + val totalVotesText = createTotalVotesText(pollState, pollResponseSummary) + + return PollItem_() + .attributes(attributes) + .eventId(informationData.eventId) + .pollQuestion(question) + .canVote(pollState.isVotable()) + .totalVotesText(totalVotesText) + .optionViewStates(optionViewStates) + .edited(informationData.hasBeenEdited) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + .callback(callback) + } + + @VisibleForTesting + private fun createPollState( + informationData: MessageInformationData, + pollResponseSummary: PollResponseData?, + pollContent: MessagePollContent, + ): PollState = when { + !informationData.sendState.isSent() -> PollState.Sending + pollResponseSummary?.isClosed.orFalse() -> PollState.Ended + pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> PollState.Undisclosed + pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> PollState.Voted(pollResponseSummary?.totalVotes ?: 0) + else -> PollState.Ready + } + + @VisibleForTesting + private fun List.mapToOptions( + pollState: PollState, + informationData: MessageInformationData, + ) = map { answer -> + val pollResponseSummary = informationData.pollResponseAggregatedSummary + val winnerVoteCount = pollResponseSummary?.winnerVoteCount + val optionId = answer.id ?: "" + val optionAnswer = answer.getBestAnswer() ?: "" + val voteSummary = pollResponseSummary?.votes?.get(answer.id) + val voteCount = voteSummary?.total ?: 0 + val votePercentage = voteSummary?.percentage ?: 0.0 + val isMyVote = pollResponseSummary?.myVote == answer.id + val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount + + when (pollState) { + PollState.Sending -> PollOptionViewState.PollSending(optionId, optionAnswer) + PollState.Ready -> PollOptionViewState.PollReady(optionId, optionAnswer) + is PollState.Voted -> PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote) + PollState.Undisclosed -> PollOptionViewState.PollUndisclosed(optionId, optionAnswer, isMyVote) + PollState.Ended -> PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner) + } + } + + private fun createPollQuestion( + informationData: MessageInformationData, + question: String, + callback: TimelineEventController.Callback?, + ) = if (informationData.hasBeenEdited) { + annotateWithEdited(stringProvider, colorProvider, dimensionConverter, question, callback, informationData) + } else { + question + }.toEpoxyCharSequence() + + private fun createTotalVotesText( + pollState: PollState, + pollResponseSummary: PollResponseData?, + ): String { + val votes = pollResponseSummary?.totalVotes ?: 0 + return when { + pollState is PollState.Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes) + pollState is PollState.Undisclosed -> "" + pollState is PollState.Voted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, votes, votes) + votes == 0 -> stringProvider.getString(R.string.poll_no_votes_cast) + else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes) + } + } +} From 77dfd5f8264dc9ce4e457c41755dbd8f57692ff3 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 21 Jun 2022 17:26:26 +0300 Subject: [PATCH 02/24] Create initial test class. --- .../timeline/factory/PollItemFactoryTest.kt | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt new file mode 100644 index 0000000000..70268a70f8 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -0,0 +1,75 @@ +/* + * 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.home.room.detail.timeline.factory + +import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.matrix.android.sdk.api.session.room.send.SendState + +private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( + eventId = "eventId", + senderId = "senderId", + ageLocalTS = 0, + avatarUrl = "", + sendState = SendState.SENDING, + messageLayout = TimelineMessageLayout.Default(showAvatar = true, showDisplayName = true, showTimestamp = true), + reactionsSummary = ReactionsSummaryData(), + sentByMe = true, +) + +class PollItemFactoryTest { + + private val testDispatcher = UnconfinedTestDispatcher() + + @get:Rule + val mvRxTestRule = MvRxTestRule( + testDispatcher = testDispatcher // See https://github.com/airbnb/mavericks/issues/599 + ) + + private lateinit var pollItemFactory: PollItemFactory + + @Before + fun setup() { + // We are not going to test any UI related code + pollItemFactory = PollItemFactory( + stringProvider = mockk(), + avatarSizeProvider = mockk(), + colorProvider = mockk(), + dimensionConverter = mockk(), + ) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given ` = runTest { + + } +} From a886e93c7ea0522687d825645538e230abd0bb09 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 12:13:53 +0300 Subject: [PATCH 03/24] Test sending poll state. --- .../room/detail/timeline/factory/PollItemFactory.kt | 8 ++++---- .../detail/timeline/factory/PollItemFactoryTest.kt | 11 +++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt index 09b601f080..dbed274838 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt @@ -73,8 +73,8 @@ class PollItemFactory @Inject constructor( .callback(callback) } - @VisibleForTesting - private fun createPollState( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun createPollState( informationData: MessageInformationData, pollResponseSummary: PollResponseData?, pollContent: MessagePollContent, @@ -86,8 +86,8 @@ class PollItemFactory @Inject constructor( else -> PollState.Ready } - @VisibleForTesting - private fun List.mapToOptions( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun List.mapToOptions( pollState: PollState, informationData: MessageInformationData, ) = map { answer -> diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 70268a70f8..5fab6a6b0a 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -20,14 +20,17 @@ import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import im.vector.app.features.poll.PollState import io.mockk.mockk import io.mockk.unmockkAll import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.send.SendState private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( @@ -69,7 +72,11 @@ class PollItemFactoryTest { } @Test - fun `given ` = runTest { - + fun `given a sending poll state then returns PollState as Sending`() = runTest { + pollItemFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = null, + pollContent = MessagePollContent() + ) shouldBe PollState.Sending } } From 8854b81977ed65208f0a6a8a7e8dc5d243003cb6 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 12:34:52 +0300 Subject: [PATCH 04/24] Test ended poll state. --- .../timeline/factory/PollItemFactoryTest.kt | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 5fab6a6b0a..d27736043b 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.factory import com.airbnb.mvrx.test.MvRxTestRule 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.ReactionsSummaryData import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.poll.PollState @@ -38,12 +39,17 @@ private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( senderId = "senderId", ageLocalTS = 0, avatarUrl = "", - sendState = SendState.SENDING, + sendState = SendState.SENT, messageLayout = TimelineMessageLayout.Default(showAvatar = true, showDisplayName = true, showTimestamp = true), reactionsSummary = ReactionsSummaryData(), sentByMe = true, ) +private val A_POLL_RESPONSE_DATA = PollResponseData( + myVote = null, + votes = emptyMap(), +) + class PollItemFactoryTest { private val testDispatcher = UnconfinedTestDispatcher() @@ -72,11 +78,23 @@ class PollItemFactoryTest { } @Test - fun `given a sending poll state then returns PollState as Sending`() = runTest { + fun `given a sending poll state then PollState is Sending`() = runTest { + val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENT) pollItemFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = null, + informationData = sendingPollInformationData, + pollResponseSummary = A_POLL_RESPONSE_DATA, pollContent = MessagePollContent() ) shouldBe PollState.Sending } + + @Test + fun `given a sent poll state when poll is closed then PollState is Ended`() = runTest { + val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true) + + pollItemFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = closedPollSummary, + pollContent = MessagePollContent() + ) shouldBe PollState.Ended + } } From 0fe4b9f07febc6308ab6805e6fd1c94d02ad65ec Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 12:57:50 +0300 Subject: [PATCH 05/24] Test undisclosed poll state. --- .../room/model/message/PollCreationInfo.kt | 5 ++- .../timeline/factory/PollItemFactory.kt | 2 +- .../timeline/factory/PollItemFactoryTest.kt | 41 ++++++++++++++++++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt index 81b034a809..ee31d5959e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt @@ -25,4 +25,7 @@ data class PollCreationInfo( @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE, @Json(name = "max_selections") val maxSelections: Int = 1, @Json(name = "answers") val answers: List? = null -) +) { + + fun isUndisclosed() = kind in listOf(PollType.UNDISCLOSED_UNSTABLE, PollType.UNDISCLOSED) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt index dbed274838..842eeb65d8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt @@ -81,7 +81,7 @@ class PollItemFactory @Inject constructor( ): PollState = when { !informationData.sendState.isSent() -> PollState.Sending pollResponseSummary?.isClosed.orFalse() -> PollState.Ended - pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> PollState.Undisclosed + pollContent.getBestPollCreationInfo()?.isUndisclosed().orFalse() -> PollState.Undisclosed pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> PollState.Voted(pollResponseSummary?.totalVotes ?: 0) else -> PollState.Ready } diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index d27736043b..051386007f 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -32,6 +32,10 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +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.PollQuestion +import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.send.SendState private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( @@ -50,6 +54,30 @@ private val A_POLL_RESPONSE_DATA = PollResponseData( votes = emptyMap(), ) +private val A_POLL_CONTENT = MessagePollContent( + unstablePollCreationInfo = PollCreationInfo( + question = PollQuestion( + unstableQuestion = "What is your favourite coffee?" + ), + kind = PollType.UNDISCLOSED_UNSTABLE, + maxSelections = 1, + answers = listOf( + PollAnswer( + id = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", + unstableAnswer = "Double Espresso" + ), + PollAnswer( + id = "ec1a4db0-46d8-4d7a-9bb6-d80724715938", + unstableAnswer = "Macchiato" + ), + PollAnswer( + id = "3677ca8e-061b-40ab-bffe-b22e4e88fcad", + unstableAnswer = "Iced Coffee" + ), + ) + ) +) + class PollItemFactoryTest { private val testDispatcher = UnconfinedTestDispatcher() @@ -83,7 +111,7 @@ class PollItemFactoryTest { pollItemFactory.createPollState( informationData = sendingPollInformationData, pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = MessagePollContent() + pollContent = A_POLL_CONTENT ) shouldBe PollState.Sending } @@ -94,7 +122,16 @@ class PollItemFactoryTest { pollItemFactory.createPollState( informationData = A_MESSAGE_INFORMATION_DATA, pollResponseSummary = closedPollSummary, - pollContent = MessagePollContent() + pollContent = A_POLL_CONTENT ) shouldBe PollState.Ended } + + @Test + fun `given a sent poll when undisclosed poll type is selected then PollState is Undisclosed`() = runTest { + pollItemFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = A_POLL_RESPONSE_DATA, + pollContent = A_POLL_CONTENT + ) shouldBe PollState.Undisclosed + } } From 2c5ddca8219bf8e56cae7dabc8bb0f078316aef1 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 13:21:50 +0300 Subject: [PATCH 06/24] Test voted poll state. --- .../timeline/factory/PollItemFactoryTest.kt | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 051386007f..50ddb0afb9 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -27,6 +27,7 @@ import io.mockk.unmockkAll import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Before import org.junit.Rule @@ -107,11 +108,11 @@ class PollItemFactoryTest { @Test fun `given a sending poll state then PollState is Sending`() = runTest { - val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENT) + val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENDING) pollItemFactory.createPollState( informationData = sendingPollInformationData, pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = A_POLL_CONTENT + pollContent = A_POLL_CONTENT, ) shouldBe PollState.Sending } @@ -122,7 +123,7 @@ class PollItemFactoryTest { pollItemFactory.createPollState( informationData = A_MESSAGE_INFORMATION_DATA, pollResponseSummary = closedPollSummary, - pollContent = A_POLL_CONTENT + pollContent = A_POLL_CONTENT, ) shouldBe PollState.Ended } @@ -131,7 +132,26 @@ class PollItemFactoryTest { pollItemFactory.createPollState( informationData = A_MESSAGE_INFORMATION_DATA, pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = A_POLL_CONTENT + pollContent = A_POLL_CONTENT, ) shouldBe PollState.Undisclosed } + + @Test + fun `given a sent poll when my vote exists then PollState is Voted`() = runTest { + val votedPollData = A_POLL_RESPONSE_DATA.copy( + totalVotes = 1, + myVote = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", + ) + val disclosedPollContent = A_POLL_CONTENT.copy( + unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( + kind = PollType.DISCLOSED_UNSTABLE + ) + ) + + pollItemFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = votedPollData, + pollContent = disclosedPollContent, + ) shouldBeEqualTo PollState.Voted(1) + } } From 5a948891f0f8b6b6524d1bde06a81c4fdfd57ffe Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 13:27:52 +0300 Subject: [PATCH 07/24] Test ready poll state. --- .../timeline/factory/PollItemFactoryTest.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 50ddb0afb9..156cde6ff7 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -154,4 +154,19 @@ class PollItemFactoryTest { pollContent = disclosedPollContent, ) shouldBeEqualTo PollState.Voted(1) } + + @Test + fun `given a sent poll when poll type is disclosed then PollState is Ready`() = runTest { + val disclosedPollContent = A_POLL_CONTENT.copy( + unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( + kind = PollType.DISCLOSED_UNSTABLE + ) + ) + + pollItemFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = A_POLL_RESPONSE_DATA, + pollContent = disclosedPollContent, + ) shouldBe PollState.Ready + } } From 2cf40cbcf2a9b1e27f48a08210211721fdfd3a33 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 14:05:42 +0300 Subject: [PATCH 08/24] Test sending option view states. --- .../detail/timeline/factory/PollItemFactory.kt | 1 - .../timeline/factory/PollItemFactoryTest.kt | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt index 842eeb65d8..05e945c193 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt @@ -35,7 +35,6 @@ import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.PollAnswer -import org.matrix.android.sdk.api.session.room.model.message.PollType import javax.inject.Inject class PollItemFactory @Inject constructor( diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 156cde6ff7..d7cd757a39 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.factory import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout @@ -169,4 +170,19 @@ class PollItemFactoryTest { pollContent = disclosedPollContent, ) shouldBe PollState.Ready } + + @Test + fun `given a sending poll then all option view states is PollSending`() = runTest { + with(pollItemFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") + } + } + } + } } From 0f0492db3b21eaf69daac418560d6f22e17d4436 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 15:27:04 +0300 Subject: [PATCH 09/24] Test ready option view states. --- .../timeline/factory/PollItemFactoryTest.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index d7cd757a39..02a24d8ca7 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -185,4 +185,19 @@ class PollItemFactoryTest { } } } + + @Test + fun `given a sent poll then all option view states is PollReady`() = runTest { + with(pollItemFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") + } + } + } + } } From 8bb421a916afc43f6d2cad996d7e12935180e8b8 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 15:44:28 +0300 Subject: [PATCH 10/24] Test poll voted option view states. --- .../timeline/factory/PollItemFactoryTest.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 02a24d8ca7..acd84eb4b0 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -200,4 +200,26 @@ class PollItemFactoryTest { } } } + + @Test + fun `given a sent poll when a vote is cast then all option view states is PollVoted`() = runTest { + with(pollItemFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Voted(1), A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollVoted( + optionId = option.id ?: "", + optionAnswer = option.getBestAnswer() ?: "", + voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0, + votePercentage = voteSummary?.percentage ?: 0.0, + isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, + ) + } + } + } + } } From d0d2929a84e4b27f37a19f02a59620bee0b14587 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 16:29:19 +0300 Subject: [PATCH 11/24] Test undisclosed option view states. --- .../timeline/factory/PollItemFactoryTest.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index acd84eb4b0..8209f992e0 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -222,4 +222,23 @@ class PollItemFactoryTest { } } } + + @Test + fun `given a sent poll when the poll is undisclosed then all option view states is PollUndisclosed`() = runTest { + with(pollItemFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Undisclosed, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollUndisclosed( + optionId = option.id ?: "", + optionAnswer = option.getBestAnswer() ?: "", + isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, + ) + } + } + } + } } From aab558af0926ae89f4ebdd68bf462ae7631e08a9 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 16:40:11 +0300 Subject: [PATCH 12/24] Test ended poll option view states. --- .../timeline/factory/PollItemFactoryTest.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 8209f992e0..6c4f9e872e 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -241,4 +241,28 @@ class PollItemFactoryTest { } } } + + @Test + fun `given an ended poll then all option view states is Ended`() = runTest { + with(pollItemFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Ended, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) + val voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0 + val winnerVoteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.winnerVoteCount ?: 0 + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollEnded( + optionId = option.id ?: "", + optionAnswer = option.getBestAnswer() ?: "", + voteCount = voteCount, + votePercentage = voteSummary?.percentage ?: 0.0, + isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount, + ) + } + } + } + } } From a7bc2ef3bc8451e1a09df2fd69af95c1acc81c56 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 16:45:22 +0300 Subject: [PATCH 13/24] Changelog added. --- changelog.d/6366.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6366.misc diff --git a/changelog.d/6366.misc b/changelog.d/6366.misc new file mode 100644 index 0000000000..5752b3d700 --- /dev/null +++ b/changelog.d/6366.misc @@ -0,0 +1 @@ +Poll view state unit tests From 2be43e929435ff665167c574e89ff0e94896db91 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 17:18:17 +0300 Subject: [PATCH 14/24] Test isVotable function. --- .../detail/timeline/factory/PollItemFactoryTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 6c4f9e872e..be397e25ea 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -265,4 +265,18 @@ class PollItemFactoryTest { } } } + + @Test + fun `given a poll state when it is not Sending and not Ended then the poll is votable`() = runTest { + val sendingPollState = PollState.Sending + sendingPollState.isVotable() shouldBe false + val readyPollState = PollState.Ready + readyPollState.isVotable() shouldBe true + val votedPollState = PollState.Voted(1) + votedPollState.isVotable() shouldBe true + val undisclosedPollState = PollState.Undisclosed + undisclosedPollState.isVotable() shouldBe true + var endedPollState = PollState.Ended + endedPollState.isVotable() shouldBe false + } } From 532bc18b1e5ac8a6baa79350c7fbc551e9ca8656 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 24 Jun 2022 16:52:16 +0300 Subject: [PATCH 15/24] Refactor poll item view state factory. --- .../timeline/factory/MessageItemFactory.kt | 28 +- .../timeline/factory/PollItemFactory.kt | 135 --------- .../factory/PollItemViewStateFactory.kt | 185 ++++++++++++ .../timeline/item/MessageInformationData.kt | 5 +- .../poll/{PollState.kt => PollViewState.kt} | 16 +- .../timeline/factory/PollItemFactoryTest.kt | 282 ------------------ 6 files changed, 223 insertions(+), 428 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt rename vector/src/main/java/im/vector/app/features/poll/{PollState.kt => PollViewState.kt} (66%) delete mode 100644 vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt 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 fba6ffbe51..d2032bb4c4 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 @@ -53,6 +53,8 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_ +import im.vector.app.features.home.room.detail.timeline.item.PollItem +import im.vector.app.features.home.room.detail.timeline.item.PollItem_ import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem @@ -128,7 +130,7 @@ class MessageItemFactory @Inject constructor( private val vectorPreferences: VectorPreferences, private val urlMapProvider: UrlMapProvider, private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory, - private val pollItemFactory: PollItemFactory, + private val pollItemViewStateFactory: PollItemViewStateFactory, ) { // TODO inject this properly? @@ -188,7 +190,7 @@ class MessageItemFactory @Inject constructor( is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollContent -> pollItemFactory.create(messageContent, informationData, highlight, callback, attributes) + is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes) is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) @@ -224,6 +226,28 @@ class MessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) } + private fun buildPollItem( + pollContent: MessagePollContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): PollItem { + val pollViewState = pollItemViewStateFactory.create(pollContent, informationData, callback) + + return PollItem_() + .attributes(attributes) + .eventId(informationData.eventId) + .pollQuestion(pollViewState.question) + .canVote(pollViewState.canVote) + .totalVotesText(pollViewState.totalVotes) + .optionViewStates(pollViewState.optionViewStates) + .edited(informationData.hasBeenEdited) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + .callback(callback) + } + private fun buildAudioMessageItem( params: TimelineItemFactoryParams, messageContent: MessageAudioContent, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt deleted file mode 100644 index 05e945c193..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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.home.room.detail.timeline.factory - -import androidx.annotation.VisibleForTesting -import im.vector.app.R -import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.DimensionConverter -import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.factory.MessageItemFactoryHelper.annotateWithEdited -import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider -import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem -import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.app.features.home.room.detail.timeline.item.PollItem_ -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState -import im.vector.app.features.home.room.detail.timeline.item.PollResponseData -import im.vector.app.features.poll.PollState -import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent -import org.matrix.android.sdk.api.session.room.model.message.PollAnswer -import javax.inject.Inject - -class PollItemFactory @Inject constructor( - private val stringProvider: StringProvider, - private val avatarSizeProvider: AvatarSizeProvider, - private val colorProvider: ColorProvider, - private val dimensionConverter: DimensionConverter, -) { - - fun create( - pollContent: MessagePollContent, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes, - ): VectorEpoxyModel<*>? { - val pollResponseSummary = informationData.pollResponseAggregatedSummary - val pollState = createPollState(informationData, pollResponseSummary, pollContent) - val pollCreationInfo = pollContent.getBestPollCreationInfo() - val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty() - val question = createPollQuestion(informationData, questionText, callback) - val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData) - val totalVotesText = createTotalVotesText(pollState, pollResponseSummary) - - return PollItem_() - .attributes(attributes) - .eventId(informationData.eventId) - .pollQuestion(question) - .canVote(pollState.isVotable()) - .totalVotesText(totalVotesText) - .optionViewStates(optionViewStates) - .edited(informationData.hasBeenEdited) - .highlighted(highlight) - .leftGuideline(avatarSizeProvider.leftGuideline) - .callback(callback) - } - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun createPollState( - informationData: MessageInformationData, - pollResponseSummary: PollResponseData?, - pollContent: MessagePollContent, - ): PollState = when { - !informationData.sendState.isSent() -> PollState.Sending - pollResponseSummary?.isClosed.orFalse() -> PollState.Ended - pollContent.getBestPollCreationInfo()?.isUndisclosed().orFalse() -> PollState.Undisclosed - pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> PollState.Voted(pollResponseSummary?.totalVotes ?: 0) - else -> PollState.Ready - } - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun List.mapToOptions( - pollState: PollState, - informationData: MessageInformationData, - ) = map { answer -> - val pollResponseSummary = informationData.pollResponseAggregatedSummary - val winnerVoteCount = pollResponseSummary?.winnerVoteCount - val optionId = answer.id ?: "" - val optionAnswer = answer.getBestAnswer() ?: "" - val voteSummary = pollResponseSummary?.votes?.get(answer.id) - val voteCount = voteSummary?.total ?: 0 - val votePercentage = voteSummary?.percentage ?: 0.0 - val isMyVote = pollResponseSummary?.myVote == answer.id - val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount - - when (pollState) { - PollState.Sending -> PollOptionViewState.PollSending(optionId, optionAnswer) - PollState.Ready -> PollOptionViewState.PollReady(optionId, optionAnswer) - is PollState.Voted -> PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote) - PollState.Undisclosed -> PollOptionViewState.PollUndisclosed(optionId, optionAnswer, isMyVote) - PollState.Ended -> PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner) - } - } - - private fun createPollQuestion( - informationData: MessageInformationData, - question: String, - callback: TimelineEventController.Callback?, - ) = if (informationData.hasBeenEdited) { - annotateWithEdited(stringProvider, colorProvider, dimensionConverter, question, callback, informationData) - } else { - question - }.toEpoxyCharSequence() - - private fun createTotalVotesText( - pollState: PollState, - pollResponseSummary: PollResponseData?, - ): String { - val votes = pollResponseSummary?.totalVotes ?: 0 - return when { - pollState is PollState.Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes) - pollState is PollState.Undisclosed -> "" - pollState is PollState.Voted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, votes, votes) - votes == 0 -> stringProvider.getString(R.string.poll_no_votes_cast) - else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt new file mode 100644 index 0000000000..8365f0710e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -0,0 +1,185 @@ +/* + * 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.home.room.detail.timeline.factory + +import im.vector.app.R +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.factory.MessageItemFactoryHelper.annotateWithEdited +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData +import im.vector.app.features.poll.PollViewState +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo +import javax.inject.Inject + +class PollItemViewStateFactory @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, + private val dimensionConverter: DimensionConverter, +) { + + fun create( + pollContent: MessagePollContent, + informationData: MessageInformationData, + callback: TimelineEventController.Callback?, + ): PollViewState { + val pollCreationInfo = pollContent.getBestPollCreationInfo() + + val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty() + val question = createPollQuestion(informationData, questionText, callback) + + val pollResponseSummary = informationData.pollResponseAggregatedSummary + val winnerVoteCount = pollResponseSummary?.winnerVoteCount + val totalVotes = pollResponseSummary?.totalVotes ?: 0 + + return when { + !informationData.sendState.isSent() -> { + createSendingPollViewState(question, pollCreationInfo) + } + informationData.pollResponseAggregatedSummary?.isClosed.orFalse() -> { + createEndedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes, winnerVoteCount) + } + pollContent.getBestPollCreationInfo()?.isUndisclosed().orFalse() -> { + createUndisclosedPollViewState(question, pollCreationInfo, pollResponseSummary) + } + informationData.pollResponseAggregatedSummary?.myVote?.isNotEmpty().orFalse() -> { + createVotedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes) + } + else -> { + createReadyPollViewState(question, pollCreationInfo, totalVotes) + } + } + } + + private fun createSendingPollViewState(question: EpoxyCharSequence, pollCreationInfo: PollCreationInfo?): PollViewState { + return PollViewState( + question = question, + totalVotes = stringProvider.getString(R.string.poll_no_votes_cast), + canVote = false, + optionViewStates = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollSending( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "" + ) + }, + ) + } + + private fun createEndedPollViewState( + question: EpoxyCharSequence, + pollCreationInfo: PollCreationInfo?, + pollResponseSummary: PollResponseData?, + totalVotes: Int, + winnerVoteCount: Int?, + ): PollViewState { + return PollViewState( + question = question, + totalVotes = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes), + canVote = false, + optionViewStates = pollCreationInfo?.answers?.map { answer -> + val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "") + PollOptionViewState.PollEnded( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "", + voteCount = voteSummary?.total ?: 0, + votePercentage = voteSummary?.percentage ?: 0.0, + isWinner = winnerVoteCount != 0 && voteSummary?.total == winnerVoteCount + ) + }, + ) + } + + private fun createUndisclosedPollViewState( + question: EpoxyCharSequence, + pollCreationInfo: PollCreationInfo?, + pollResponseSummary: PollResponseData? + ): PollViewState { + return PollViewState( + question = question, + totalVotes = "", + canVote = true, + optionViewStates = pollCreationInfo?.answers?.map { answer -> + val isMyVote = pollResponseSummary?.myVote == answer.id + PollOptionViewState.PollUndisclosed( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "", + isSelected = isMyVote + ) + }, + ) + } + + private fun createVotedPollViewState( + question: EpoxyCharSequence, + pollCreationInfo: PollCreationInfo?, + pollResponseSummary: PollResponseData?, + totalVotes: Int + ): PollViewState { + return PollViewState( + question = question, + totalVotes = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes), + canVote = true, + optionViewStates = pollCreationInfo?.answers?.map { answer -> + val isMyVote = pollResponseSummary?.myVote == answer.id + val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "") + PollOptionViewState.PollVoted( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "", + voteCount = voteSummary?.total ?: 0, + votePercentage = voteSummary?.percentage ?: 0.0, + isSelected = isMyVote + ) + }, + ) + } + + private fun createReadyPollViewState(question: EpoxyCharSequence, pollCreationInfo: PollCreationInfo?, totalVotes: Int): PollViewState { + val totalVotesText = if (totalVotes == 0) { + stringProvider.getString(R.string.poll_no_votes_cast) + } else { + stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, totalVotes, totalVotes) + } + return PollViewState( + question = question, + totalVotes = totalVotesText, + canVote = true, + optionViewStates = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollReady( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "" + ) + }, + ) + } + + private fun createPollQuestion( + informationData: MessageInformationData, + question: String, + callback: TimelineEventController.Callback?, + ) = if (informationData.hasBeenEdited) { + annotateWithEdited(stringProvider, colorProvider, dimensionConverter, question, callback, informationData) + } else { + question + }.toEpoxyCharSequence() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index 554dd0ada8..9b24720c88 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -91,7 +91,10 @@ data class PollResponseData( val totalVotes: Int = 0, val winnerVoteCount: Int = 0, val isClosed: Boolean = false -) : Parcelable +) : Parcelable { + + fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId) +} @Parcelize data class PollVoteSummaryData( diff --git a/vector/src/main/java/im/vector/app/features/poll/PollState.kt b/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt similarity index 66% rename from vector/src/main/java/im/vector/app/features/poll/PollState.kt rename to vector/src/main/java/im/vector/app/features/poll/PollViewState.kt index 93cdb0ecbe..01947d8850 100644 --- a/vector/src/main/java/im/vector/app/features/poll/PollState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt @@ -16,12 +16,12 @@ package im.vector.app.features.poll -sealed interface PollState { - object Sending : PollState - object Ready : PollState - data class Voted(val votes: Int) : PollState - object Undisclosed : PollState - object Ended : PollState +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence - fun isVotable() = this !is Sending && this !is Ended -} +data class PollViewState( + val question: EpoxyCharSequence, + val totalVotes: String, + val canVote: Boolean, + val optionViewStates: List?, +) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt deleted file mode 100644 index be397e25ea..0000000000 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ /dev/null @@ -1,282 +0,0 @@ -/* - * 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.home.room.detail.timeline.factory - -import com.airbnb.mvrx.test.MvRxTestRule -import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState -import im.vector.app.features.home.room.detail.timeline.item.PollResponseData -import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData -import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout -import im.vector.app.features.poll.PollState -import io.mockk.mockk -import io.mockk.unmockkAll -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBe -import org.amshove.kluent.shouldBeEqualTo -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent -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.PollQuestion -import org.matrix.android.sdk.api.session.room.model.message.PollType -import org.matrix.android.sdk.api.session.room.send.SendState - -private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( - eventId = "eventId", - senderId = "senderId", - ageLocalTS = 0, - avatarUrl = "", - sendState = SendState.SENT, - messageLayout = TimelineMessageLayout.Default(showAvatar = true, showDisplayName = true, showTimestamp = true), - reactionsSummary = ReactionsSummaryData(), - sentByMe = true, -) - -private val A_POLL_RESPONSE_DATA = PollResponseData( - myVote = null, - votes = emptyMap(), -) - -private val A_POLL_CONTENT = MessagePollContent( - unstablePollCreationInfo = PollCreationInfo( - question = PollQuestion( - unstableQuestion = "What is your favourite coffee?" - ), - kind = PollType.UNDISCLOSED_UNSTABLE, - maxSelections = 1, - answers = listOf( - PollAnswer( - id = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", - unstableAnswer = "Double Espresso" - ), - PollAnswer( - id = "ec1a4db0-46d8-4d7a-9bb6-d80724715938", - unstableAnswer = "Macchiato" - ), - PollAnswer( - id = "3677ca8e-061b-40ab-bffe-b22e4e88fcad", - unstableAnswer = "Iced Coffee" - ), - ) - ) -) - -class PollItemFactoryTest { - - private val testDispatcher = UnconfinedTestDispatcher() - - @get:Rule - val mvRxTestRule = MvRxTestRule( - testDispatcher = testDispatcher // See https://github.com/airbnb/mavericks/issues/599 - ) - - private lateinit var pollItemFactory: PollItemFactory - - @Before - fun setup() { - // We are not going to test any UI related code - pollItemFactory = PollItemFactory( - stringProvider = mockk(), - avatarSizeProvider = mockk(), - colorProvider = mockk(), - dimensionConverter = mockk(), - ) - } - - @After - fun tearDown() { - unmockkAll() - } - - @Test - fun `given a sending poll state then PollState is Sending`() = runTest { - val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENDING) - pollItemFactory.createPollState( - informationData = sendingPollInformationData, - pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = A_POLL_CONTENT, - ) shouldBe PollState.Sending - } - - @Test - fun `given a sent poll state when poll is closed then PollState is Ended`() = runTest { - val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true) - - pollItemFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = closedPollSummary, - pollContent = A_POLL_CONTENT, - ) shouldBe PollState.Ended - } - - @Test - fun `given a sent poll when undisclosed poll type is selected then PollState is Undisclosed`() = runTest { - pollItemFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = A_POLL_CONTENT, - ) shouldBe PollState.Undisclosed - } - - @Test - fun `given a sent poll when my vote exists then PollState is Voted`() = runTest { - val votedPollData = A_POLL_RESPONSE_DATA.copy( - totalVotes = 1, - myVote = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", - ) - val disclosedPollContent = A_POLL_CONTENT.copy( - unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( - kind = PollType.DISCLOSED_UNSTABLE - ) - ) - - pollItemFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = votedPollData, - pollContent = disclosedPollContent, - ) shouldBeEqualTo PollState.Voted(1) - } - - @Test - fun `given a sent poll when poll type is disclosed then PollState is Ready`() = runTest { - val disclosedPollContent = A_POLL_CONTENT.copy( - unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( - kind = PollType.DISCLOSED_UNSTABLE - ) - ) - - pollItemFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = disclosedPollContent, - ) shouldBe PollState.Ready - } - - @Test - fun `given a sending poll then all option view states is PollSending`() = runTest { - with(pollItemFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") - } - } - } - } - - @Test - fun `given a sent poll then all option view states is PollReady`() = runTest { - with(pollItemFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") - } - } - } - } - - @Test - fun `given a sent poll when a vote is cast then all option view states is PollVoted`() = runTest { - with(pollItemFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Voted(1), A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollVoted( - optionId = option.id ?: "", - optionAnswer = option.getBestAnswer() ?: "", - voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0, - votePercentage = voteSummary?.percentage ?: 0.0, - isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, - ) - } - } - } - } - - @Test - fun `given a sent poll when the poll is undisclosed then all option view states is PollUndisclosed`() = runTest { - with(pollItemFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Undisclosed, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollUndisclosed( - optionId = option.id ?: "", - optionAnswer = option.getBestAnswer() ?: "", - isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, - ) - } - } - } - } - - @Test - fun `given an ended poll then all option view states is Ended`() = runTest { - with(pollItemFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Ended, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) - val voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0 - val winnerVoteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.winnerVoteCount ?: 0 - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollEnded( - optionId = option.id ?: "", - optionAnswer = option.getBestAnswer() ?: "", - voteCount = voteCount, - votePercentage = voteSummary?.percentage ?: 0.0, - isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount, - ) - } - } - } - } - - @Test - fun `given a poll state when it is not Sending and not Ended then the poll is votable`() = runTest { - val sendingPollState = PollState.Sending - sendingPollState.isVotable() shouldBe false - val readyPollState = PollState.Ready - readyPollState.isVotable() shouldBe true - val votedPollState = PollState.Voted(1) - votedPollState.isVotable() shouldBe true - val undisclosedPollState = PollState.Undisclosed - undisclosedPollState.isVotable() shouldBe true - var endedPollState = PollState.Ended - endedPollState.isVotable() shouldBe false - } -} From e63fa2d83ffd7bdbe6bc462a4418390d048a8536 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 24 Jun 2022 17:28:59 +0300 Subject: [PATCH 16/24] Move epoxy related poll functions back to MessageItemFactory. --- .../timeline/factory/MessageItemFactory.kt | 12 +++++++- .../factory/PollItemViewStateFactory.kt | 30 ++++--------------- .../vector/app/features/poll/PollViewState.kt | 3 +- 3 files changed, 18 insertions(+), 27 deletions(-) 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 d2032bb4c4..00c7d41160 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 @@ -238,7 +238,7 @@ class MessageItemFactory @Inject constructor( return PollItem_() .attributes(attributes) .eventId(informationData.eventId) - .pollQuestion(pollViewState.question) + .pollQuestion(createPollQuestion(informationData, pollViewState.question, callback)) .canVote(pollViewState.canVote) .totalVotesText(pollViewState.totalVotes) .optionViewStates(pollViewState.optionViewStates) @@ -248,6 +248,16 @@ class MessageItemFactory @Inject constructor( .callback(callback) } + private fun createPollQuestion( + informationData: MessageInformationData, + question: String, + callback: TimelineEventController.Callback?, + ) = if (informationData.hasBeenEdited) { + annotateWithEdited(stringProvider, colorProvider, dimensionConverter, question, callback, informationData) + } else { + question + }.toEpoxyCharSequence() + private fun buildAudioMessageItem( params: TimelineItemFactoryParams, messageContent: MessageAudioContent, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt index 8365f0710e..acedd79b1c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -17,17 +17,12 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R -import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.factory.MessageItemFactoryHelper.annotateWithEdited import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.poll.PollViewState -import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence -import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo @@ -35,8 +30,6 @@ import javax.inject.Inject class PollItemViewStateFactory @Inject constructor( private val stringProvider: StringProvider, - private val colorProvider: ColorProvider, - private val dimensionConverter: DimensionConverter, ) { fun create( @@ -46,8 +39,7 @@ class PollItemViewStateFactory @Inject constructor( ): PollViewState { val pollCreationInfo = pollContent.getBestPollCreationInfo() - val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty() - val question = createPollQuestion(informationData, questionText, callback) + val question = pollCreationInfo?.question?.getBestQuestion().orEmpty() val pollResponseSummary = informationData.pollResponseAggregatedSummary val winnerVoteCount = pollResponseSummary?.winnerVoteCount @@ -72,7 +64,7 @@ class PollItemViewStateFactory @Inject constructor( } } - private fun createSendingPollViewState(question: EpoxyCharSequence, pollCreationInfo: PollCreationInfo?): PollViewState { + private fun createSendingPollViewState(question: String, pollCreationInfo: PollCreationInfo?): PollViewState { return PollViewState( question = question, totalVotes = stringProvider.getString(R.string.poll_no_votes_cast), @@ -87,7 +79,7 @@ class PollItemViewStateFactory @Inject constructor( } private fun createEndedPollViewState( - question: EpoxyCharSequence, + question: String, pollCreationInfo: PollCreationInfo?, pollResponseSummary: PollResponseData?, totalVotes: Int, @@ -111,7 +103,7 @@ class PollItemViewStateFactory @Inject constructor( } private fun createUndisclosedPollViewState( - question: EpoxyCharSequence, + question: String, pollCreationInfo: PollCreationInfo?, pollResponseSummary: PollResponseData? ): PollViewState { @@ -131,7 +123,7 @@ class PollItemViewStateFactory @Inject constructor( } private fun createVotedPollViewState( - question: EpoxyCharSequence, + question: String, pollCreationInfo: PollCreationInfo?, pollResponseSummary: PollResponseData?, totalVotes: Int @@ -154,7 +146,7 @@ class PollItemViewStateFactory @Inject constructor( ) } - private fun createReadyPollViewState(question: EpoxyCharSequence, pollCreationInfo: PollCreationInfo?, totalVotes: Int): PollViewState { + private fun createReadyPollViewState(question: String, pollCreationInfo: PollCreationInfo?, totalVotes: Int): PollViewState { val totalVotesText = if (totalVotes == 0) { stringProvider.getString(R.string.poll_no_votes_cast) } else { @@ -172,14 +164,4 @@ class PollItemViewStateFactory @Inject constructor( }, ) } - - private fun createPollQuestion( - informationData: MessageInformationData, - question: String, - callback: TimelineEventController.Callback?, - ) = if (informationData.hasBeenEdited) { - annotateWithEdited(stringProvider, colorProvider, dimensionConverter, question, callback, informationData) - } else { - question - }.toEpoxyCharSequence() } diff --git a/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt index 01947d8850..0f01d58c96 100644 --- a/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt @@ -17,10 +17,9 @@ package im.vector.app.features.poll import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState -import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence data class PollViewState( - val question: EpoxyCharSequence, + val question: String, val totalVotes: String, val canVote: Boolean, val optionViewStates: List?, From a9358e98b2a40436f53ba8f6ca5b277038427f8c Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 27 Jun 2022 13:39:21 +0300 Subject: [PATCH 17/24] Fix sending poll unit test. --- .../timeline/factory/MessageItemFactory.kt | 2 +- .../factory/PollItemViewStateFactory.kt | 2 - .../factory/PollItemViewStateFactoryTest.kt | 286 ++++++++++++++++++ 3 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt 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 00c7d41160..9b6186c878 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 @@ -233,7 +233,7 @@ class MessageItemFactory @Inject constructor( callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes, ): PollItem { - val pollViewState = pollItemViewStateFactory.create(pollContent, informationData, callback) + val pollViewState = pollItemViewStateFactory.create(pollContent, informationData) return PollItem_() .attributes(attributes) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt index acedd79b1c..8da0f2d279 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R import im.vector.app.core.resources.StringProvider -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState import im.vector.app.features.home.room.detail.timeline.item.PollResponseData @@ -35,7 +34,6 @@ class PollItemViewStateFactory @Inject constructor( fun create( pollContent: MessagePollContent, informationData: MessageInformationData, - callback: TimelineEventController.Callback?, ): PollViewState { val pollCreationInfo = pollContent.getBestPollCreationInfo() diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt new file mode 100644 index 0000000000..3f73129bc5 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -0,0 +1,286 @@ +/* + * 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.home.room.detail.timeline.factory + +import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.R +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData +import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import im.vector.app.features.poll.PollViewState +import im.vector.app.test.fakes.FakeStringProvider +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +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.PollQuestion +import org.matrix.android.sdk.api.session.room.model.message.PollType +import org.matrix.android.sdk.api.session.room.send.SendState + +private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( + eventId = "eventId", + senderId = "senderId", + ageLocalTS = 0, + avatarUrl = "", + sendState = SendState.SENT, + messageLayout = TimelineMessageLayout.Default(showAvatar = true, showDisplayName = true, showTimestamp = true), + reactionsSummary = ReactionsSummaryData(), + sentByMe = true, +) + +private val A_POLL_RESPONSE_DATA = PollResponseData( + myVote = null, + votes = emptyMap(), +) + +private val A_POLL_CONTENT = MessagePollContent( + unstablePollCreationInfo = PollCreationInfo( + question = PollQuestion( + unstableQuestion = "What is your favourite coffee?" + ), + kind = PollType.UNDISCLOSED_UNSTABLE, + maxSelections = 1, + answers = listOf( + PollAnswer( + id = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", + unstableAnswer = "Double Espresso" + ), + PollAnswer( + id = "ec1a4db0-46d8-4d7a-9bb6-d80724715938", + unstableAnswer = "Macchiato" + ), + PollAnswer( + id = "3677ca8e-061b-40ab-bffe-b22e4e88fcad", + unstableAnswer = "Iced Coffee" + ), + ) + ) +) + +class PollItemViewStateFactoryTest { + + private val testDispatcher = UnconfinedTestDispatcher() + + @get:Rule + val mvRxTestRule = MvRxTestRule( + testDispatcher = testDispatcher // See https://github.com/airbnb/mavericks/issues/599 + ) + private lateinit var pollItemViewStateFactory: PollItemViewStateFactory + + private val stringProvider = FakeStringProvider() + + @Before + fun setup() { + // We are not going to test any UI related code + pollItemViewStateFactory = PollItemViewStateFactory( + stringProvider = stringProvider.instance, + ) + } + + @Test + fun `given a sending poll state then poll is not votable and option states are PollSending`() = runTest { + val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENDING) + val pollViewState = pollItemViewStateFactory.create( + pollContent = A_POLL_CONTENT, + informationData = sendingPollInformationData, + ) + + pollViewState shouldBeEqualTo PollViewState( + question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", + totalVotes = stringProvider.instance.getString(R.string.poll_no_votes_cast), + canVote = false, + optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> + PollOptionViewState.PollSending( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "" + ) + }, + ) + } + + /* + @Test + fun `given a sent poll state when poll is closed then PollState is Ended`() = runTest { + val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true) + + pollItemViewStateFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = closedPollSummary, + pollContent = A_POLL_CONTENT, + ) shouldBe PollState.Ended + } + + @Test + fun `given a sent poll when undisclosed poll type is selected then PollState is Undisclosed`() = runTest { + pollItemViewStateFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = A_POLL_RESPONSE_DATA, + pollContent = A_POLL_CONTENT, + ) shouldBe PollState.Undisclosed + } + + @Test + fun `given a sent poll when my vote exists then PollState is Voted`() = runTest { + val votedPollData = A_POLL_RESPONSE_DATA.copy( + totalVotes = 1, + myVote = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", + ) + val disclosedPollContent = A_POLL_CONTENT.copy( + unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( + kind = PollType.DISCLOSED_UNSTABLE + ) + ) + + pollItemViewStateFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = votedPollData, + pollContent = disclosedPollContent, + ) shouldBeEqualTo PollState.Voted(1) + } + + @Test + fun `given a sent poll when poll type is disclosed then PollState is Ready`() = runTest { + val disclosedPollContent = A_POLL_CONTENT.copy( + unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( + kind = PollType.DISCLOSED_UNSTABLE + ) + ) + + pollItemViewStateFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = A_POLL_RESPONSE_DATA, + pollContent = disclosedPollContent, + ) shouldBe PollState.Ready + } + + @Test + fun `given a sending poll then all option view states is PollSending`() = runTest { + with(pollItemViewStateFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") + } + } + } + } + + @Test + fun `given a sent poll then all option view states is PollReady`() = runTest { + with(pollItemViewStateFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") + } + } + } + } + + @Test + fun `given a sent poll when a vote is cast then all option view states is PollVoted`() = runTest { + with(pollItemViewStateFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Voted(1), A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollVoted( + optionId = option.id ?: "", + optionAnswer = option.getBestAnswer() ?: "", + voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0, + votePercentage = voteSummary?.percentage ?: 0.0, + isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, + ) + } + } + } + } + + @Test + fun `given a sent poll when the poll is undisclosed then all option view states is PollUndisclosed`() = runTest { + with(pollItemViewStateFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Undisclosed, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollUndisclosed( + optionId = option.id ?: "", + optionAnswer = option.getBestAnswer() ?: "", + isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, + ) + } + } + } + } + + @Test + fun `given an ended poll then all option view states is Ended`() = runTest { + with(pollItemViewStateFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Ended, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) + val voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0 + val winnerVoteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.winnerVoteCount ?: 0 + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollEnded( + optionId = option.id ?: "", + optionAnswer = option.getBestAnswer() ?: "", + voteCount = voteCount, + votePercentage = voteSummary?.percentage ?: 0.0, + isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount, + ) + } + } + } + } + + @Test + fun `given a poll state when it is not Sending and not Ended then the poll is votable`() = runTest { + val sendingPollState = PollState.Sending + sendingPollState.isVotable() shouldBe false + val readyPollState = PollState.Ready + readyPollState.isVotable() shouldBe true + val votedPollState = PollState.Voted(1) + votedPollState.isVotable() shouldBe true + val undisclosedPollState = PollState.Undisclosed + undisclosedPollState.isVotable() shouldBe true + var endedPollState = PollState.Ended + endedPollState.isVotable() shouldBe false + } + */ +} From 1a668da07703d0cea9c97156037b10b17406da3a Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 27 Jun 2022 13:57:35 +0300 Subject: [PATCH 18/24] Fix ended poll unit test. --- .../factory/PollItemViewStateFactoryTest.kt | 27 ++++++++++++++----- .../app/test/fakes/FakeStringProvider.kt | 4 +++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index 3f73129bc5..fa36ddb71e 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -119,18 +119,33 @@ class PollItemViewStateFactoryTest { ) } - /* @Test - fun `given a sent poll state when poll is closed then PollState is Ended`() = runTest { + fun `given a sent poll state when poll is closed then poll is not votable and option states are Ended`() = runTest { val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true) + val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary) - pollItemViewStateFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = closedPollSummary, + val pollViewState = pollItemViewStateFactory.create( pollContent = A_POLL_CONTENT, - ) shouldBe PollState.Ended + informationData = closedPollInformationData, + ) + + pollViewState shouldBeEqualTo PollViewState( + question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", + totalVotes = stringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_after_ended, 0, 0), + canVote = false, + optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> + PollOptionViewState.PollEnded( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "", + voteCount = 0, + votePercentage = 0.0, + isWinner = false + ) + }, + ) } + /* @Test fun `given a sent poll when undisclosed poll type is selected then PollState is Undisclosed`() = runTest { pollItemViewStateFactory.createPollState( diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt index e63550abe0..28d9f7c732 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeStringProvider.kt @@ -27,6 +27,10 @@ class FakeStringProvider { every { instance.getString(any()) } answers { "test-${args[0]}" } + + every { instance.getQuantityString(any(), any(), any()) } answers { + "test-${args[0]}-${args[1]}" + } } fun given(id: Int, result: String) { From 6f4e079a2e175d0e1f85b9bb19aac96a06beaf3d Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 27 Jun 2022 14:07:26 +0300 Subject: [PATCH 19/24] Fix undisclosed poll unit test. --- .../factory/PollItemViewStateFactoryTest.kt | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index fa36ddb71e..a1c332c029 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -145,16 +145,28 @@ class PollItemViewStateFactoryTest { ) } - /* @Test - fun `given a sent poll when undisclosed poll type is selected then PollState is Undisclosed`() = runTest { - pollItemViewStateFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = A_POLL_RESPONSE_DATA, + fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() = runTest { + val pollViewState = pollItemViewStateFactory.create( pollContent = A_POLL_CONTENT, - ) shouldBe PollState.Undisclosed + informationData = A_MESSAGE_INFORMATION_DATA, + ) + + pollViewState shouldBeEqualTo PollViewState( + question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", + totalVotes = "", + canVote = true, + optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> + PollOptionViewState.PollUndisclosed( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "", + isSelected = false + ) + }, + ) } + /* @Test fun `given a sent poll when my vote exists then PollState is Voted`() = runTest { val votedPollData = A_POLL_RESPONSE_DATA.copy( From d7c2dbe83f53e43a57bf08316f29408bec31ce1f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 27 Jun 2022 14:48:58 +0300 Subject: [PATCH 20/24] Fix voted poll unit test. --- .../factory/PollItemViewStateFactoryTest.kt | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index a1c332c029..0d28b34414 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -21,6 +21,7 @@ import im.vector.app.R import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState 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.ReactionsSummaryData import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.poll.PollViewState @@ -54,6 +55,8 @@ private val A_POLL_RESPONSE_DATA = PollResponseData( votes = emptyMap(), ) +private val A_POLL_OPTION_IDS = listOf("5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", "ec1a4db0-46d8-4d7a-9bb6-d80724715938", "3677ca8e-061b-40ab-bffe-b22e4e88fcad") + private val A_POLL_CONTENT = MessagePollContent( unstablePollCreationInfo = PollCreationInfo( question = PollQuestion( @@ -63,15 +66,15 @@ private val A_POLL_CONTENT = MessagePollContent( maxSelections = 1, answers = listOf( PollAnswer( - id = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", + id = A_POLL_OPTION_IDS[0], unstableAnswer = "Double Espresso" ), PollAnswer( - id = "ec1a4db0-46d8-4d7a-9bb6-d80724715938", + id =A_POLL_OPTION_IDS[1], unstableAnswer = "Macchiato" ), PollAnswer( - id = "3677ca8e-061b-40ab-bffe-b22e4e88fcad", + id = A_POLL_OPTION_IDS[2], unstableAnswer = "Iced Coffee" ), ) @@ -166,26 +169,42 @@ class PollItemViewStateFactoryTest { ) } - /* @Test - fun `given a sent poll when my vote exists then PollState is Voted`() = runTest { + fun `given a sent poll when my vote exists then poll is still votable and options states are PollVoted`() = runTest { val votedPollData = A_POLL_RESPONSE_DATA.copy( totalVotes = 1, - myVote = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", + myVote = A_POLL_OPTION_IDS[0], + votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)) ) val disclosedPollContent = A_POLL_CONTENT.copy( unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( kind = PollType.DISCLOSED_UNSTABLE - ) + ), + ) + val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData) + + val pollViewState = pollItemViewStateFactory.create( + pollContent = disclosedPollContent, + informationData = votedInformationData, ) - pollItemViewStateFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = votedPollData, - pollContent = disclosedPollContent, - ) shouldBeEqualTo PollState.Voted(1) + pollViewState shouldBeEqualTo PollViewState( + question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", + totalVotes = stringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, 1, 1), + canVote = true, + optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.mapIndexed { index, answer -> + PollOptionViewState.PollVoted( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "", + voteCount = if (index == 0) 1 else 0, + votePercentage = if (index == 0) 1.0 else 0.0, + isSelected = index == 0 + ) + }, + ) } + /* @Test fun `given a sent poll when poll type is disclosed then PollState is Ready`() = runTest { val disclosedPollContent = A_POLL_CONTENT.copy( From 4f9b36134685251ef14b1ff99a927302150ec82b Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 27 Jun 2022 15:06:08 +0300 Subject: [PATCH 21/24] Fix ready poll unit test. --- .../factory/PollItemViewStateFactoryTest.kt | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index 0d28b34414..3ebf545765 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -204,22 +204,32 @@ class PollItemViewStateFactoryTest { ) } - /* @Test - fun `given a sent poll when poll type is disclosed then PollState is Ready`() = runTest { + fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() = runTest { val disclosedPollContent = A_POLL_CONTENT.copy( unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( kind = PollType.DISCLOSED_UNSTABLE ) ) - - pollItemViewStateFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = A_POLL_RESPONSE_DATA, + val pollViewState = pollItemViewStateFactory.create( pollContent = disclosedPollContent, - ) shouldBe PollState.Ready + informationData = A_MESSAGE_INFORMATION_DATA, + ) + + pollViewState shouldBeEqualTo PollViewState( + question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", + totalVotes = stringProvider.instance.getString(R.string.poll_no_votes_cast), + canVote = true, + optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> + PollOptionViewState.PollReady( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "" + ) + }, + ) } + /* @Test fun `given a sending poll then all option view states is PollSending`() = runTest { with(pollItemViewStateFactory) { From 8100a2e6747c43ab786115b51a68fd23ef55c764 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 27 Jun 2022 15:19:26 +0300 Subject: [PATCH 22/24] Remove duplicated unit tests. --- .../factory/PollItemViewStateFactoryTest.kt | 113 +----------------- 1 file changed, 1 insertion(+), 112 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index 3ebf545765..1f6020e639 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -70,7 +70,7 @@ private val A_POLL_CONTENT = MessagePollContent( unstableAnswer = "Double Espresso" ), PollAnswer( - id =A_POLL_OPTION_IDS[1], + id = A_POLL_OPTION_IDS[1], unstableAnswer = "Macchiato" ), PollAnswer( @@ -228,115 +228,4 @@ class PollItemViewStateFactoryTest { }, ) } - - /* - @Test - fun `given a sending poll then all option view states is PollSending`() = runTest { - with(pollItemViewStateFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") - } - } - } - } - - @Test - fun `given a sent poll then all option view states is PollReady`() = runTest { - with(pollItemViewStateFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") - } - } - } - } - - @Test - fun `given a sent poll when a vote is cast then all option view states is PollVoted`() = runTest { - with(pollItemViewStateFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Voted(1), A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollVoted( - optionId = option.id ?: "", - optionAnswer = option.getBestAnswer() ?: "", - voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0, - votePercentage = voteSummary?.percentage ?: 0.0, - isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, - ) - } - } - } - } - - @Test - fun `given a sent poll when the poll is undisclosed then all option view states is PollUndisclosed`() = runTest { - with(pollItemViewStateFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Undisclosed, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollUndisclosed( - optionId = option.id ?: "", - optionAnswer = option.getBestAnswer() ?: "", - isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, - ) - } - } - } - } - - @Test - fun `given an ended poll then all option view states is Ended`() = runTest { - with(pollItemViewStateFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Ended, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) - val voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0 - val winnerVoteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.winnerVoteCount ?: 0 - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollEnded( - optionId = option.id ?: "", - optionAnswer = option.getBestAnswer() ?: "", - voteCount = voteCount, - votePercentage = voteSummary?.percentage ?: 0.0, - isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount, - ) - } - } - } - } - - @Test - fun `given a poll state when it is not Sending and not Ended then the poll is votable`() = runTest { - val sendingPollState = PollState.Sending - sendingPollState.isVotable() shouldBe false - val readyPollState = PollState.Ready - readyPollState.isVotable() shouldBe true - val votedPollState = PollState.Voted(1) - votedPollState.isVotable() shouldBe true - val undisclosedPollState = PollState.Undisclosed - undisclosedPollState.isVotable() shouldBe true - var endedPollState = PollState.Ended - endedPollState.isVotable() shouldBe false - } - */ } From 30115fa2b982451b63b4c574a869a9c128a24c7e Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 27 Jun 2022 15:43:21 +0300 Subject: [PATCH 23/24] Code review fixes. --- .../timeline/factory/MessageItemFactory.kt | 56 ++++++++++++- .../factory/MessageItemFactoryHelper.kt | 82 ------------------- .../factory/PollItemViewStateFactoryTest.kt | 20 ++--- 3 files changed, 57 insertions(+), 101 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt 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 9b6186c878..853fef8bc8 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 @@ -16,8 +16,13 @@ package im.vector.app.features.home.room.detail.timeline.factory +import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned +import android.text.TextPaint +import android.text.style.AbsoluteSizeSpan +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan import android.view.View import dagger.Lazy import im.vector.app.R @@ -30,7 +35,6 @@ import im.vector.app.core.time.Clock import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.containsOnlyEmojis import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.factory.MessageItemFactoryHelper.annotateWithEdited import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder @@ -253,7 +257,7 @@ class MessageItemFactory @Inject constructor( question: String, callback: TimelineEventController.Callback?, ) = if (informationData.hasBeenEdited) { - annotateWithEdited(stringProvider, colorProvider, dimensionConverter, question, callback, informationData) + annotateWithEdited(question, callback, informationData) } else { question }.toEpoxyCharSequence() @@ -554,7 +558,7 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .message( if (informationData.hasBeenEdited) { - annotateWithEdited(stringProvider, colorProvider, dimensionConverter, linkifiedBody, callback, informationData) + annotateWithEdited(linkifiedBody, callback, informationData) } else { linkifiedBody }.toEpoxyCharSequence() @@ -572,6 +576,50 @@ class MessageItemFactory @Inject constructor( .movementMethod(createLinkMovementMethod(callback)) } + private fun annotateWithEdited( + linkifiedBody: CharSequence, + callback: TimelineEventController.Callback?, + informationData: MessageInformationData, + ): Spannable { + val spannable = SpannableStringBuilder() + spannable.append(linkifiedBody) + val editedSuffix = stringProvider.getString(R.string.edited_suffix) + spannable.append(" ").append(editedSuffix) + val color = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) + val editStart = spannable.lastIndexOf(editedSuffix) + val editEnd = editStart + editedSuffix.length + spannable.setSpan( + ForegroundColorSpan(color), + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + + // Note: text size is set to 14sp + spannable.setSpan( + AbsoluteSizeSpan(dimensionConverter.spToPx(13)), + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + + spannable.setSpan( + object : ClickableSpan() { + override fun onClick(widget: View) { + callback?.onEditedDecorationClicked(informationData) + } + + override fun updateDrawState(ds: TextPaint) { + // nop + } + }, + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + return spannable + } + private fun buildNoticeMessageItem( messageContent: MessageNoticeContent, @Suppress("UNUSED_PARAMETER") @@ -618,7 +666,7 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .message( if (informationData.hasBeenEdited) { - annotateWithEdited(stringProvider, colorProvider, dimensionConverter, message, callback, informationData) + annotateWithEdited(message, callback, informationData) } else { message }.toEpoxyCharSequence() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt deleted file mode 100644 index 0c4c7238e7..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.home.room.detail.timeline.factory - -import android.text.Spannable -import android.text.SpannableStringBuilder -import android.text.Spanned -import android.text.TextPaint -import android.text.style.AbsoluteSizeSpan -import android.text.style.ClickableSpan -import android.text.style.ForegroundColorSpan -import android.view.View -import im.vector.app.R -import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.DimensionConverter -import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData - -object MessageItemFactoryHelper { - - fun annotateWithEdited( - stringProvider: StringProvider, - colorProvider: ColorProvider, - dimensionConverter: DimensionConverter, - linkifiedBody: CharSequence, - callback: TimelineEventController.Callback?, - informationData: MessageInformationData, - ): Spannable { - val spannable = SpannableStringBuilder() - spannable.append(linkifiedBody) - val editedSuffix = stringProvider.getString(R.string.edited_suffix) - spannable.append(" ").append(editedSuffix) - val color = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) - val editStart = spannable.lastIndexOf(editedSuffix) - val editEnd = editStart + editedSuffix.length - spannable.setSpan( - ForegroundColorSpan(color), - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - - // Note: text size is set to 14sp - spannable.setSpan( - AbsoluteSizeSpan(dimensionConverter.spToPx(13)), - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - - spannable.setSpan( - object : ClickableSpan() { - override fun onClick(widget: View) { - callback?.onEditedDecorationClicked(informationData) - } - - override fun updateDrawState(ds: TextPaint) { - // nop - } - }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - return spannable - } -} diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index 1f6020e639..e519652503 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -16,7 +16,6 @@ package im.vector.app.features.home.room.detail.timeline.factory -import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.R import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState @@ -26,11 +25,8 @@ import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryDat import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.poll.PollViewState import im.vector.app.test.fakes.FakeStringProvider -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Before -import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.PollAnswer @@ -83,12 +79,6 @@ private val A_POLL_CONTENT = MessagePollContent( class PollItemViewStateFactoryTest { - private val testDispatcher = UnconfinedTestDispatcher() - - @get:Rule - val mvRxTestRule = MvRxTestRule( - testDispatcher = testDispatcher // See https://github.com/airbnb/mavericks/issues/599 - ) private lateinit var pollItemViewStateFactory: PollItemViewStateFactory private val stringProvider = FakeStringProvider() @@ -102,7 +92,7 @@ class PollItemViewStateFactoryTest { } @Test - fun `given a sending poll state then poll is not votable and option states are PollSending`() = runTest { + fun `given a sending poll state then poll is not votable and option states are PollSending`() { val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENDING) val pollViewState = pollItemViewStateFactory.create( pollContent = A_POLL_CONTENT, @@ -123,7 +113,7 @@ class PollItemViewStateFactoryTest { } @Test - fun `given a sent poll state when poll is closed then poll is not votable and option states are Ended`() = runTest { + fun `given a sent poll state when poll is closed then poll is not votable and option states are Ended`() { val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true) val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary) @@ -149,7 +139,7 @@ class PollItemViewStateFactoryTest { } @Test - fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() = runTest { + fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() { val pollViewState = pollItemViewStateFactory.create( pollContent = A_POLL_CONTENT, informationData = A_MESSAGE_INFORMATION_DATA, @@ -170,7 +160,7 @@ class PollItemViewStateFactoryTest { } @Test - fun `given a sent poll when my vote exists then poll is still votable and options states are PollVoted`() = runTest { + fun `given a sent poll when my vote exists then poll is still votable and options states are PollVoted`() { val votedPollData = A_POLL_RESPONSE_DATA.copy( totalVotes = 1, myVote = A_POLL_OPTION_IDS[0], @@ -205,7 +195,7 @@ class PollItemViewStateFactoryTest { } @Test - fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() = runTest { + fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() { val disclosedPollContent = A_POLL_CONTENT.copy( unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( kind = PollType.DISCLOSED_UNSTABLE From 863cc7e0fc4ebeff351186343ecc18cdedec11f0 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 27 Jun 2022 18:37:03 +0300 Subject: [PATCH 24/24] Code review fix. --- .../factory/PollItemViewStateFactoryTest.kt | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index e519652503..64ad03a019 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -26,7 +26,6 @@ import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLay import im.vector.app.features.poll.PollViewState import im.vector.app.test.fakes.FakeStringProvider import org.amshove.kluent.shouldBeEqualTo -import org.junit.Before import org.junit.Test import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.PollAnswer @@ -79,20 +78,11 @@ private val A_POLL_CONTENT = MessagePollContent( class PollItemViewStateFactoryTest { - private lateinit var pollItemViewStateFactory: PollItemViewStateFactory - - private val stringProvider = FakeStringProvider() - - @Before - fun setup() { - // We are not going to test any UI related code - pollItemViewStateFactory = PollItemViewStateFactory( - stringProvider = stringProvider.instance, - ) - } - @Test fun `given a sending poll state then poll is not votable and option states are PollSending`() { + val stringProvider = FakeStringProvider() + val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) + val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENDING) val pollViewState = pollItemViewStateFactory.create( pollContent = A_POLL_CONTENT, @@ -114,6 +104,9 @@ class PollItemViewStateFactoryTest { @Test fun `given a sent poll state when poll is closed then poll is not votable and option states are Ended`() { + val stringProvider = FakeStringProvider() + val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) + val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true) val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary) @@ -140,6 +133,9 @@ class PollItemViewStateFactoryTest { @Test fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() { + val stringProvider = FakeStringProvider() + val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) + val pollViewState = pollItemViewStateFactory.create( pollContent = A_POLL_CONTENT, informationData = A_MESSAGE_INFORMATION_DATA, @@ -161,6 +157,9 @@ class PollItemViewStateFactoryTest { @Test fun `given a sent poll when my vote exists then poll is still votable and options states are PollVoted`() { + val stringProvider = FakeStringProvider() + val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) + val votedPollData = A_POLL_RESPONSE_DATA.copy( totalVotes = 1, myVote = A_POLL_OPTION_IDS[0], @@ -196,6 +195,9 @@ class PollItemViewStateFactoryTest { @Test fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() { + val stringProvider = FakeStringProvider() + val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) + val disclosedPollContent = A_POLL_CONTENT.copy( unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( kind = PollType.DISCLOSED_UNSTABLE