mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Refactor poll item factory to make it testable.
This commit is contained in:
parent
41431cd1d2
commit
bd9fa48312
3 changed files with 223 additions and 156 deletions
|
@ -16,13 +16,8 @@
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.factory
|
package im.vector.app.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
import android.text.Spannable
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
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 android.view.View
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import im.vector.app.R
|
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.DimensionConverter
|
||||||
import im.vector.app.core.utils.containsOnlyEmojis
|
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.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.AudioMessagePlaybackTracker
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
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.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.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.RedactedMessageItem_
|
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
|
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.location.toLocationData
|
||||||
import im.vector.app.features.media.ImageContentRenderer
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
import im.vector.app.features.media.VideoContentRenderer
|
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.settings.VectorPreferences
|
||||||
import im.vector.app.features.voice.AudioWaveformView
|
import im.vector.app.features.voice.AudioWaveformView
|
||||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
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.Session
|
||||||
import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt
|
import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt
|
||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
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.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
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.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.getFileUrl
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
|
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||||
|
@ -149,6 +128,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val urlMapProvider: UrlMapProvider,
|
private val urlMapProvider: UrlMapProvider,
|
||||||
private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory,
|
private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory,
|
||||||
|
private val pollItemFactory: PollItemFactory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// TODO inject this properly?
|
// TODO inject this properly?
|
||||||
|
@ -208,7 +188,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
|
is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes)
|
||||||
is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes)
|
is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes)
|
||||||
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, 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 MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
|
||||||
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
|
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
|
||||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
|
@ -244,93 +224,6 @@ class MessageItemFactory @Inject constructor(
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.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<PollAnswer>.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(
|
private fun buildAudioMessageItem(
|
||||||
params: TimelineItemFactoryParams,
|
params: TimelineItemFactoryParams,
|
||||||
messageContent: MessageAudioContent,
|
messageContent: MessageAudioContent,
|
||||||
|
@ -627,7 +520,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.message(
|
.message(
|
||||||
if (informationData.hasBeenEdited) {
|
if (informationData.hasBeenEdited) {
|
||||||
annotateWithEdited(linkifiedBody, callback, informationData)
|
annotateWithEdited(stringProvider, colorProvider, dimensionConverter, linkifiedBody, callback, informationData)
|
||||||
} else {
|
} else {
|
||||||
linkifiedBody
|
linkifiedBody
|
||||||
}.toEpoxyCharSequence()
|
}.toEpoxyCharSequence()
|
||||||
|
@ -645,50 +538,6 @@ class MessageItemFactory @Inject constructor(
|
||||||
.movementMethod(createLinkMovementMethod(callback))
|
.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(
|
private fun buildNoticeMessageItem(
|
||||||
messageContent: MessageNoticeContent,
|
messageContent: MessageNoticeContent,
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
@ -735,7 +584,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.message(
|
.message(
|
||||||
if (informationData.hasBeenEdited) {
|
if (informationData.hasBeenEdited) {
|
||||||
annotateWithEdited(message, callback, informationData)
|
annotateWithEdited(stringProvider, colorProvider, dimensionConverter, message, callback, informationData)
|
||||||
} else {
|
} else {
|
||||||
message
|
message
|
||||||
}.toEpoxyCharSequence()
|
}.toEpoxyCharSequence()
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<PollAnswer>.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue