From bb2f5a133082656dd824f28de67cbda21a93c4b8 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 21 Aug 2023 14:10:21 +0200 Subject: [PATCH] Render ended poll with winning answers --- .../components/event/TimelineItemPollView.kt | 5 +- .../event/TimelineItemContentPollFactory.kt | 19 +++-- .../model/event/TimelineItemPollContent.kt | 1 + .../event/TimelineItemPollContentProvider.kt | 1 + .../features/poll/api/PollAnswerItem.kt | 4 + .../features/poll/api/PollAnswerView.kt | 78 ++++++++++++++++--- .../poll/api/PollAnswerViewProvider.kt | 14 +++- ...ePollContentView.kt => PollContentView.kt} | 31 ++++++-- 8 files changed, 122 insertions(+), 31 deletions(-) rename features/poll/api/src/main/kotlin/io/element/android/features/poll/api/{ActivePollContentView.kt => PollContentView.kt} (84%) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt index db3503be37..3608593cde 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider -import io.element.android.features.poll.api.ActivePollContentView +import io.element.android.features.poll.api.PollContentView import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.matrix.api.poll.PollAnswer @@ -33,10 +33,11 @@ fun TimelineItemPollView( onAnswerSelected: (PollAnswer) -> Unit, modifier: Modifier = Modifier, ) { - ActivePollContentView( + PollContentView( question = content.question, answerItems = content.answerItems.toImmutableList(), pollKind = content.pollKind, + isPollEnded = content.isEnded, onAnswerSelected = onAnswerSelected, modifier = modifier, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt index 656cb0910f..df15d2868a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt @@ -38,19 +38,23 @@ class TimelineItemContentPollFactory @Inject constructor( // Todo Move this computation to the matrix rust sdk val pollVotesCount = content.votes.flatMap { it.value }.size val userVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys + val isEndedPoll = content.endTime != null + val winnerIds = content.answers.map { it.id } + .groupBy { content.votes[it]?.size ?: 0 } // Group by votes count + .maxBy { it.key } // Keep max voted answers + .takeIf { it.key > 0 } // Ignore if no option has been voted + ?.value.orEmpty() val answerItems = content.answers.map { answer -> val votesCount = content.votes[answer.id]?.size ?: 0 val isSelected = answer.id in userVotes - val percentage = when { - pollVotesCount == 0 -> 0f - content.kind.isDisclosed -> votesCount.toFloat() / pollVotesCount.toFloat() - isSelected -> 1f - else -> 0f - } + val isWinner = answer.id in winnerIds + val percentage = if (pollVotesCount > 0) votesCount.toFloat() / pollVotesCount.toFloat() else 0f PollAnswerItem( answer = answer, isSelected = isSelected, - isDisclosed = content.kind.isDisclosed, + isEnabled = isEndedPoll, + isWinner = isWinner, + isDisclosed = content.kind.isDisclosed || isEndedPoll, votesCount = votesCount, percentage = percentage, ) @@ -61,6 +65,7 @@ class TimelineItemContentPollFactory @Inject constructor( answerItems = answerItems, votes = content.votes, pollKind = content.kind, + isEnded = isEndedPoll, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt index fcbe81b8da..3c0e0edfd4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt @@ -25,6 +25,7 @@ data class TimelineItemPollContent( val answerItems: List, val votes: Map>, val pollKind: PollKind, + val isEnded: Boolean, ) : TimelineItemEventContent { override val type: String = "TimelineItemPollContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt index 01253f6e77..49dfa58c8a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt @@ -33,6 +33,7 @@ fun aTimelineItemPollContent(): TimelineItemPollContent { pollKind = PollKind.Disclosed, question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(), + isEnded = false, votes = emptyMap(), ) } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt index eb6e131068..1955701c5b 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt @@ -23,6 +23,8 @@ import io.element.android.libraries.matrix.api.poll.PollAnswer * * @property answer the poll answer. * @property isSelected whether the user has selected this answer. + * @property isEnabled whether the answer can be voted. + * @property isWinner whether this is the winner answer in the poll. * @property isDisclosed whether the votes for this answer should be disclosed. * @property votesCount the number of votes for this answer. * @property percentage the percentage of votes for this answer. @@ -30,6 +32,8 @@ import io.element.android.libraries.matrix.api.poll.PollAnswer data class PollAnswerItem( val answer: PollAnswer, val isSelected: Boolean, + val isEnabled: Boolean, + val isWinner: Boolean, val isDisclosed: Boolean, val votesCount: Int, val percentage: Float, diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt index c87d95be3a..3e52cc5fc7 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt @@ -34,13 +34,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.preview.DayNightPreviews -import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconToggleButton import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.toEnabledColor import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonPlurals @@ -55,6 +56,7 @@ fun PollAnswerView( .fillMaxWidth() .selectable( selected = answerItem.isSelected, + enabled = answerItem.isEnabled, onClick = onClick, role = Role.RadioButton, ) @@ -62,6 +64,7 @@ fun PollAnswerView( IconToggleButton( modifier = Modifier.size(22.dp), checked = answerItem.isSelected, + enabled = answerItem.isEnabled, colors = IconButtonDefaults.iconToggleButtonColors( contentColor = ElementTheme.colors.iconSecondary, checkedContentColor = ElementTheme.colors.iconPrimary, @@ -83,7 +86,8 @@ fun PollAnswerView( Row { Text( modifier = Modifier.weight(1f), - text = answerItem.answer.text + text = answerItem.answer.text, + style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular, ) if (answerItem.isDisclosed) { Text( @@ -93,35 +97,85 @@ fun PollAnswerView( count = answerItem.votesCount, answerItem.votesCount ), - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textSecondary, + style = if (answerItem.isWinner) ElementTheme.typography.fontBodySmMedium else ElementTheme.typography.fontBodySmRegular, + color = if (answerItem.isWinner) ElementTheme.colors.textPrimary else ElementTheme.colors.textSecondary, ) } } Spacer(modifier = Modifier.height(10.dp)) LinearProgressIndicator( modifier = Modifier.fillMaxWidth(), - progress = answerItem.percentage, + color = if (answerItem.isWinner) ElementTheme.colors.textSuccessPrimary else answerItem.isEnabled.toEnabledColor(), + progress = when { + answerItem.isDisclosed -> answerItem.percentage + answerItem.isSelected -> 1f + else -> 0f + }, strokeCap = StrokeCap.Round, ) } } } -@DayNightPreviews +@Preview +@Composable +internal fun PollAnswerDisclosedNotSelectedPreview() = ElementThemedPreview { + PollAnswerView( + answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false), + onClick = { }, + ) +} + +@Preview +@Composable +internal fun PollAnswerDisclosedSelectedPreview() = ElementThemedPreview { + PollAnswerView( + answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true), + onClick = { } + ) +} + +@Preview @Composable -internal fun PollAnswerViewNoResultsPreview() = ElementPreview { +internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementThemedPreview { PollAnswerView( - answerItem = aPollAnswerItem(isSelected = true), + answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false), onClick = { }, ) } -@DayNightPreviews +@Preview +@Composable +internal fun PollAnswerUndisclosedSelectedPreview() = ElementThemedPreview { + PollAnswerView( + answerItem = aPollAnswerItem(isDisclosed = false, isSelected = true), + onClick = { } + ) +} + +@Preview +@Composable +internal fun PollAnswerEndedWinnerNotSelectedPreview() = ElementThemedPreview { + PollAnswerView( + answerItem = aPollAnswerItem(isDisclosed = true, isSelected = false, isEnabled = false, isWinner = true), + onClick = { } + ) +} + +@Preview +@Composable +internal fun PollAnswerEndedWinnerSelectedPreview() = ElementThemedPreview { + PollAnswerView( + answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = true), + onClick = { } + ) +} + +@Preview @Composable -internal fun PollAnswerViewWithResultPreview() = ElementPreview { +internal fun PollAnswerEndedSelectedPreview() = ElementThemedPreview { PollAnswerView( - answerItem = aPollAnswerItem(isDisclosed = false), + answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = false), onClick = { } ) } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt index 271b6bef3a..e94b5adeeb 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt @@ -19,27 +19,33 @@ package io.element.android.features.poll.api import io.element.android.libraries.matrix.api.poll.PollAnswer import kotlinx.collections.immutable.persistentListOf -fun aPollAnswerItemList(isDisclosed: Boolean = true) = persistentListOf( +fun aPollAnswerItemList(isEnded: Boolean = false, isDisclosed: Boolean = true) = persistentListOf( aPollAnswerItem( answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"), isDisclosed = isDisclosed, + isEnabled = !isEnded, + isWinner = isEnded, votesCount = 5, percentage = 0.5f ), aPollAnswerItem( answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"), isDisclosed = isDisclosed, + isEnabled = !isEnded, + isWinner = false, votesCount = 0, percentage = 0f ), aPollAnswerItem( answer = PollAnswer("option_3", "Brazilian \uD83C\uDDE7\uD83C\uDDF7"), isDisclosed = isDisclosed, + isEnabled = !isEnded, + isWinner = false, isSelected = true, votesCount = 1, percentage = 0.1f ), - aPollAnswerItem(isDisclosed = isDisclosed), + aPollAnswerItem(isDisclosed = isDisclosed, isEnabled = !isEnded), ) fun aPollAnswerItem( @@ -48,12 +54,16 @@ fun aPollAnswerItem( "French \uD83C\uDDEB\uD83C\uDDF7 But make it a very very very long option then this should just keep expanding" ), isSelected: Boolean = false, + isEnabled: Boolean = true, + isWinner: Boolean = false, isDisclosed: Boolean = true, votesCount: Int = 4, percentage: Float = 0.4f, ) = PollAnswerItem( answer = answer, isSelected = isSelected, + isEnabled = isEnabled, + isWinner = isWinner, isDisclosed = isDisclosed, votesCount = votesCount, percentage = percentage diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt similarity index 84% rename from features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt rename to features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt index c9aa822632..f778f66bd1 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt @@ -42,10 +42,11 @@ import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList @Composable -fun ActivePollContentView( +fun PollContentView( question: String, answerItems: ImmutableList, pollKind: PollKind, + isPollEnded: Boolean, onAnswerSelected: (PollAnswer) -> Unit, modifier: Modifier = Modifier, ) { @@ -59,9 +60,9 @@ fun ActivePollContentView( PollAnswers(answerItems = answerItems, onAnswerSelected = onAnswerSelected) - when (pollKind) { - PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems) - PollKind.Undisclosed -> UndisclosedPollBottomNotice() + when { + isPollEnded || pollKind == PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems) + pollKind == PollKind.Undisclosed -> UndisclosedPollBottomNotice() } } } @@ -126,22 +127,36 @@ fun ColumnScope.UndisclosedPollBottomNotice() { @DayNightPreviews @Composable -internal fun ActivePollContentNoResultsPreview() = ElementPreview { - ActivePollContentView( +internal fun PollContentNoResultsPreview() = ElementPreview { + PollContentView( question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(isDisclosed = false), pollKind = PollKind.Undisclosed, + isPollEnded = false, onAnswerSelected = { }, ) } @DayNightPreviews @Composable -internal fun ActivePollContentWithResultsPreview() = ElementPreview { - ActivePollContentView( +internal fun PollContentWithResultsPreview() = ElementPreview { + PollContentView( question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(), pollKind = PollKind.Disclosed, + isPollEnded = false, + onAnswerSelected = { }, + ) +} + +@DayNightPreviews +@Composable +internal fun PollContentEndedPreview() = ElementPreview { + PollContentView( + question = "What type of food should we have at the party?", + answerItems = aPollAnswerItemList(isEnded = true), + pollKind = PollKind.Disclosed, + isPollEnded = false, onAnswerSelected = { }, ) }