Browse Source

Render ended poll with winning answers

pull/1113/head
Florian Renaud 1 year ago
parent
commit
bb2f5a1330
  1. 5
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt
  2. 19
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt
  3. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt
  4. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt
  5. 4
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerItem.kt
  6. 78
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerView.kt
  7. 14
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollAnswerViewProvider.kt
  8. 31
      features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt

5
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 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.TimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider 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.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollAnswer
@ -33,10 +33,11 @@ fun TimelineItemPollView(
onAnswerSelected: (PollAnswer) -> Unit, onAnswerSelected: (PollAnswer) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
ActivePollContentView( PollContentView(
question = content.question, question = content.question,
answerItems = content.answerItems.toImmutableList(), answerItems = content.answerItems.toImmutableList(),
pollKind = content.pollKind, pollKind = content.pollKind,
isPollEnded = content.isEnded,
onAnswerSelected = onAnswerSelected, onAnswerSelected = onAnswerSelected,
modifier = modifier, modifier = modifier,
) )

19
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 // Todo Move this computation to the matrix rust sdk
val pollVotesCount = content.votes.flatMap { it.value }.size val pollVotesCount = content.votes.flatMap { it.value }.size
val userVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys 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 answerItems = content.answers.map { answer ->
val votesCount = content.votes[answer.id]?.size ?: 0 val votesCount = content.votes[answer.id]?.size ?: 0
val isSelected = answer.id in userVotes val isSelected = answer.id in userVotes
val percentage = when { val isWinner = answer.id in winnerIds
pollVotesCount == 0 -> 0f val percentage = if (pollVotesCount > 0) votesCount.toFloat() / pollVotesCount.toFloat() else 0f
content.kind.isDisclosed -> votesCount.toFloat() / pollVotesCount.toFloat()
isSelected -> 1f
else -> 0f
}
PollAnswerItem( PollAnswerItem(
answer = answer, answer = answer,
isSelected = isSelected, isSelected = isSelected,
isDisclosed = content.kind.isDisclosed, isEnabled = isEndedPoll,
isWinner = isWinner,
isDisclosed = content.kind.isDisclosed || isEndedPoll,
votesCount = votesCount, votesCount = votesCount,
percentage = percentage, percentage = percentage,
) )
@ -61,6 +65,7 @@ class TimelineItemContentPollFactory @Inject constructor(
answerItems = answerItems, answerItems = answerItems,
votes = content.votes, votes = content.votes,
pollKind = content.kind, pollKind = content.kind,
isEnded = isEndedPoll,
) )
} }
} }

1
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<PollAnswerItem>, val answerItems: List<PollAnswerItem>,
val votes: Map<String, List<UserId>>, val votes: Map<String, List<UserId>>,
val pollKind: PollKind, val pollKind: PollKind,
val isEnded: Boolean,
) : TimelineItemEventContent { ) : TimelineItemEventContent {
override val type: String = "TimelineItemPollContent" override val type: String = "TimelineItemPollContent"
} }

1
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, pollKind = PollKind.Disclosed,
question = "What type of food should we have at the party?", question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(), answerItems = aPollAnswerItemList(),
isEnded = false,
votes = emptyMap(), votes = emptyMap(),
) )
} }

4
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 answer the poll answer.
* @property isSelected whether the user has selected this 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 isDisclosed whether the votes for this answer should be disclosed.
* @property votesCount the number of votes for this answer. * @property votesCount the number of votes for this answer.
* @property percentage the percentage 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( data class PollAnswerItem(
val answer: PollAnswer, val answer: PollAnswer,
val isSelected: Boolean, val isSelected: Boolean,
val isEnabled: Boolean,
val isWinner: Boolean,
val isDisclosed: Boolean, val isDisclosed: Boolean,
val votesCount: Int, val votesCount: Int,
val percentage: Float, val percentage: Float,

78
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.graphics.StrokeCap
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.Icon 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.IconToggleButton
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Text 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.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonPlurals
@ -55,6 +56,7 @@ fun PollAnswerView(
.fillMaxWidth() .fillMaxWidth()
.selectable( .selectable(
selected = answerItem.isSelected, selected = answerItem.isSelected,
enabled = answerItem.isEnabled,
onClick = onClick, onClick = onClick,
role = Role.RadioButton, role = Role.RadioButton,
) )
@ -62,6 +64,7 @@ fun PollAnswerView(
IconToggleButton( IconToggleButton(
modifier = Modifier.size(22.dp), modifier = Modifier.size(22.dp),
checked = answerItem.isSelected, checked = answerItem.isSelected,
enabled = answerItem.isEnabled,
colors = IconButtonDefaults.iconToggleButtonColors( colors = IconButtonDefaults.iconToggleButtonColors(
contentColor = ElementTheme.colors.iconSecondary, contentColor = ElementTheme.colors.iconSecondary,
checkedContentColor = ElementTheme.colors.iconPrimary, checkedContentColor = ElementTheme.colors.iconPrimary,
@ -83,7 +86,8 @@ fun PollAnswerView(
Row { Row {
Text( Text(
modifier = Modifier.weight(1f), 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) { if (answerItem.isDisclosed) {
Text( Text(
@ -93,35 +97,85 @@ fun PollAnswerView(
count = answerItem.votesCount, count = answerItem.votesCount,
answerItem.votesCount answerItem.votesCount
), ),
style = ElementTheme.typography.fontBodySmRegular, style = if (answerItem.isWinner) ElementTheme.typography.fontBodySmMedium else ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary, color = if (answerItem.isWinner) ElementTheme.colors.textPrimary else ElementTheme.colors.textSecondary,
) )
} }
} }
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(), 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, 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 @Composable
internal fun PollAnswerViewNoResultsPreview() = ElementPreview { internal fun PollAnswerUndisclosedNotSelectedPreview() = ElementThemedPreview {
PollAnswerView( PollAnswerView(
answerItem = aPollAnswerItem(isSelected = true), answerItem = aPollAnswerItem(isDisclosed = false, isSelected = false),
onClick = { }, 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 @Composable
internal fun PollAnswerViewWithResultPreview() = ElementPreview { internal fun PollAnswerEndedSelectedPreview() = ElementThemedPreview {
PollAnswerView( PollAnswerView(
answerItem = aPollAnswerItem(isDisclosed = false), answerItem = aPollAnswerItem(isDisclosed = true, isSelected = true, isEnabled = false, isWinner = false),
onClick = { } onClick = { }
) )
} }

14
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 io.element.android.libraries.matrix.api.poll.PollAnswer
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
fun aPollAnswerItemList(isDisclosed: Boolean = true) = persistentListOf( fun aPollAnswerItemList(isEnded: Boolean = false, isDisclosed: Boolean = true) = persistentListOf(
aPollAnswerItem( aPollAnswerItem(
answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"), answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"),
isDisclosed = isDisclosed, isDisclosed = isDisclosed,
isEnabled = !isEnded,
isWinner = isEnded,
votesCount = 5, votesCount = 5,
percentage = 0.5f percentage = 0.5f
), ),
aPollAnswerItem( aPollAnswerItem(
answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"), answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"),
isDisclosed = isDisclosed, isDisclosed = isDisclosed,
isEnabled = !isEnded,
isWinner = false,
votesCount = 0, votesCount = 0,
percentage = 0f percentage = 0f
), ),
aPollAnswerItem( aPollAnswerItem(
answer = PollAnswer("option_3", "Brazilian \uD83C\uDDE7\uD83C\uDDF7"), answer = PollAnswer("option_3", "Brazilian \uD83C\uDDE7\uD83C\uDDF7"),
isDisclosed = isDisclosed, isDisclosed = isDisclosed,
isEnabled = !isEnded,
isWinner = false,
isSelected = true, isSelected = true,
votesCount = 1, votesCount = 1,
percentage = 0.1f percentage = 0.1f
), ),
aPollAnswerItem(isDisclosed = isDisclosed), aPollAnswerItem(isDisclosed = isDisclosed, isEnabled = !isEnded),
) )
fun aPollAnswerItem( 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" "French \uD83C\uDDEB\uD83C\uDDF7 But make it a very very very long option then this should just keep expanding"
), ),
isSelected: Boolean = false, isSelected: Boolean = false,
isEnabled: Boolean = true,
isWinner: Boolean = false,
isDisclosed: Boolean = true, isDisclosed: Boolean = true,
votesCount: Int = 4, votesCount: Int = 4,
percentage: Float = 0.4f, percentage: Float = 0.4f,
) = PollAnswerItem( ) = PollAnswerItem(
answer = answer, answer = answer,
isSelected = isSelected, isSelected = isSelected,
isEnabled = isEnabled,
isWinner = isWinner,
isDisclosed = isDisclosed, isDisclosed = isDisclosed,
votesCount = votesCount, votesCount = votesCount,
percentage = percentage percentage = percentage

31
features/poll/api/src/main/kotlin/io/element/android/features/poll/api/ActivePollContentView.kt → 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 import kotlinx.collections.immutable.ImmutableList
@Composable @Composable
fun ActivePollContentView( fun PollContentView(
question: String, question: String,
answerItems: ImmutableList<PollAnswerItem>, answerItems: ImmutableList<PollAnswerItem>,
pollKind: PollKind, pollKind: PollKind,
isPollEnded: Boolean,
onAnswerSelected: (PollAnswer) -> Unit, onAnswerSelected: (PollAnswer) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -59,9 +60,9 @@ fun ActivePollContentView(
PollAnswers(answerItems = answerItems, onAnswerSelected = onAnswerSelected) PollAnswers(answerItems = answerItems, onAnswerSelected = onAnswerSelected)
when (pollKind) { when {
PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems) isPollEnded || pollKind == PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems)
PollKind.Undisclosed -> UndisclosedPollBottomNotice() pollKind == PollKind.Undisclosed -> UndisclosedPollBottomNotice()
} }
} }
} }
@ -126,22 +127,36 @@ fun ColumnScope.UndisclosedPollBottomNotice() {
@DayNightPreviews @DayNightPreviews
@Composable @Composable
internal fun ActivePollContentNoResultsPreview() = ElementPreview { internal fun PollContentNoResultsPreview() = ElementPreview {
ActivePollContentView( PollContentView(
question = "What type of food should we have at the party?", question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(isDisclosed = false), answerItems = aPollAnswerItemList(isDisclosed = false),
pollKind = PollKind.Undisclosed, pollKind = PollKind.Undisclosed,
isPollEnded = false,
onAnswerSelected = { }, onAnswerSelected = { },
) )
} }
@DayNightPreviews @DayNightPreviews
@Composable @Composable
internal fun ActivePollContentWithResultsPreview() = ElementPreview { internal fun PollContentWithResultsPreview() = ElementPreview {
ActivePollContentView( PollContentView(
question = "What type of food should we have at the party?", question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(), answerItems = aPollAnswerItemList(),
pollKind = PollKind.Disclosed, 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 = { }, onAnswerSelected = { },
) )
} }
Loading…
Cancel
Save