Update UI for poll in the timeline

This commit is contained in:
Florian Renaud
2023-08-17 14:16:22 +02:00
parent 3a476f5130
commit 00752d851a
7 changed files with 109 additions and 101 deletions

View File

@@ -23,7 +23,7 @@ import io.element.android.features.poll.api.PollAnswerItem
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.poll.isDisclosed
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import javax.inject.Inject
@@ -36,16 +36,15 @@ class TimelineItemContentPollFactory @Inject constructor(
if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
// Todo Move this computation to the matrix rust sdk
val showResults = content.kind == PollKind.Disclosed && matrixClient.sessionId in content.votes.flatMap { it.value }
val pollVotesCount = content.votes.flatMap { it.value }.size
val userVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
val answerItems = content.answers.map { answer ->
val votesCount = content.votes[answer.id]?.size ?: 0
val progress = if (pollVotesCount > 0) votesCount.toFloat() / pollVotesCount.toFloat() else 0f
val progress = if (content.kind.isDisclosed && pollVotesCount > 0) votesCount.toFloat() / pollVotesCount.toFloat() else 0f
PollAnswerItem(
answer = answer,
isSelected = answer.id in userVotes,
isDisclosed = showResults,
isDisclosed = content.kind.isDisclosed,
votesCount = votesCount,
progress = progress,
)
@@ -56,7 +55,6 @@ class TimelineItemContentPollFactory @Inject constructor(
answerItems = answerItems,
votes = content.votes,
pollKind = content.kind,
isDisclosed = showResults
)
}
}

View File

@@ -25,7 +25,6 @@ data class TimelineItemPollContent(
val answerItems: List<PollAnswerItem>,
val votes: Map<String, List<UserId>>,
val pollKind: PollKind,
val isDisclosed: Boolean,
) : TimelineItemEventContent {
override val type: String = "TimelineItemPollContent"
}

View File

@@ -24,14 +24,13 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider<TimelineIt
override val values: Sequence<TimelineItemPollContent>
get() = sequenceOf(
aTimelineItemPollContent(),
aTimelineItemPollContent().copy(isDisclosed = true),
aTimelineItemPollContent().copy(pollKind = PollKind.Undisclosed),
)
}
fun aTimelineItemPollContent(): TimelineItemPollContent {
return TimelineItemPollContent(
pollKind = PollKind.Disclosed,
isDisclosed = false,
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(),
votes = emptyMap(),

View File

@@ -27,8 +27,6 @@ dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.constraintlayout.compose)
implementation(projects.libraries.matrix.api)
ksp(libs.showkase.processor)

View File

@@ -18,12 +18,14 @@ package io.element.android.features.poll.api
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BarChart
import androidx.compose.material.icons.outlined.Poll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -47,54 +49,80 @@ fun ActivePollContentView(
onAnswerSelected: (PollAnswer) -> Unit,
modifier: Modifier = Modifier,
) {
val showResults = answerItems.any { it.isSelected }
Column(
modifier = modifier
.selectableGroup()
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(imageVector = Icons.Default.BarChart, contentDescription = null)
Text(
text = question,
style = ElementTheme.typography.fontBodyLgMedium
)
}
PollTitle(title = question)
answerItems.forEach { answerItem ->
PollAnswerView(
answerItem = answerItem,
onClick = { onAnswerSelected(answerItem.answer) }
)
}
PollAnswers(answerItems = answerItems, onAnswerSelected = onAnswerSelected)
val votesCount = answerItems.sumOf { it.votesCount }
when {
pollKind == PollKind.Undisclosed -> {
Text(
modifier = Modifier
.align(Alignment.Start)
.padding(start = 32.dp),
style = ElementTheme.typography.fontBodyXsRegular,
color = ElementTheme.colors.textSecondary,
text = stringResource(CommonStrings.common_poll_undisclosed_text),
)
}
showResults -> {
Text(
modifier = Modifier.align(Alignment.End),
style = ElementTheme.typography.fontBodyXsRegular,
color = ElementTheme.colors.textSecondary,
text = stringResource(CommonStrings.common_poll_total_votes, votesCount),
)
}
when (pollKind) {
PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems)
PollKind.Undisclosed -> UndisclosedPollBottomNotice()
}
}
}
@Composable
internal fun PollTitle(
title: String,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Icon(
modifier = Modifier.size(22.dp),
imageVector = Icons.Outlined.Poll,
contentDescription = null
)
Text(
text = title,
style = ElementTheme.typography.fontBodyLgMedium
)
}
}
@Composable
internal fun PollAnswers(
answerItems: ImmutableList<PollAnswerItem>,
onAnswerSelected: (PollAnswer) -> Unit,
) {
answerItems.forEach { answerItem ->
PollAnswerView(
answerItem = answerItem,
onClick = { onAnswerSelected(answerItem.answer) }
)
}
}
@Composable
internal fun ColumnScope.DisclosedPollBottomNotice(
answerItems: ImmutableList<PollAnswerItem>,
) {
val votesCount = answerItems.sumOf { it.votesCount }
Text(
modifier = Modifier.align(Alignment.End),
style = ElementTheme.typography.fontBodyXsRegular,
color = ElementTheme.colors.textSecondary,
text = stringResource(CommonStrings.common_poll_total_votes, votesCount),
)
}
@Composable
fun ColumnScope.UndisclosedPollBottomNotice() {
Text(
modifier = Modifier
.align(Alignment.Start)
.padding(start = 34.dp),
style = ElementTheme.typography.fontBodyXsRegular,
color = ElementTheme.colors.textSecondary,
text = stringResource(CommonStrings.common_poll_undisclosed_text),
)
}
@DayNightPreviews
@Composable
internal fun ActivePollContentNoResultsPreview() = ElementPreview {

View File

@@ -16,18 +16,21 @@
package io.element.android.features.poll.api
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.selection.selectable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
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.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import androidx.constraintlayout.compose.Visibility
import io.element.android.libraries.designsystem.preview.DayNightPreviews
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
@@ -36,16 +39,14 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonPlurals
@Suppress("DestructuringDeclarationWithTooManyEntries") // This is necessary to declare the constraints ids
@Composable
fun PollAnswerView(
answerItem: PollAnswerItem,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
ConstraintLayout(
Row(
modifier
.wrapContentHeight()
.fillMaxWidth()
.selectable(
selected = answerItem.isSelected,
@@ -53,56 +54,38 @@ fun PollAnswerView(
role = Role.RadioButton,
)
) {
val (radioButton, answerText, votesText, progressBar) = createRefs()
RadioButton(
modifier = Modifier.constrainAs(radioButton) {
top.linkTo(answerText.top)
bottom.linkTo(answerText.bottom)
start.linkTo(parent.start)
end.linkTo(answerText.start)
},
modifier = Modifier.size(22.dp),
selected = answerItem.isSelected,
onClick = null // null recommended for accessibility with screenreaders
)
Text(
modifier = Modifier.constrainAs(answerText) {
width = Dimension.fillToConstraints
top.linkTo(parent.top)
start.linkTo(radioButton.end, margin = 8.dp)
end.linkTo(votesText.start)
bottom.linkTo(progressBar.top)
},
text = answerItem.answer.text,
)
Text(
modifier = Modifier.constrainAs(votesText) {
start.linkTo(answerText.end)
end.linkTo(parent.end)
bottom.linkTo(answerText.bottom)
visibility = if (answerItem.isDisclosed) Visibility.Visible else Visibility.Gone
},
text = pluralStringResource(
id = CommonPlurals.common_poll_votes_count,
count = answerItem.votesCount,
answerItem.votesCount
),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
LinearProgressIndicator(
progress = answerItem.progress,
modifier = Modifier
.constrainAs(progressBar) {
start.linkTo(answerText.start)
end.linkTo(votesText.end)
top.linkTo(answerText.bottom, margin = 10.dp)
bottom.linkTo(parent.bottom)
width = Dimension.fillToConstraints
visibility = if (answerItem.isDisclosed) Visibility.Visible else Visibility.Gone
},
strokeCap = StrokeCap.Round,
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Row {
Text(
modifier = Modifier.weight(1f),
text = answerItem.answer.text
)
if (answerItem.isDisclosed) {
Text(
modifier = Modifier.align(Alignment.Bottom),
text = pluralStringResource(
id = CommonPlurals.common_poll_votes_count,
count = answerItem.votesCount,
answerItem.votesCount
),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
}
}
Spacer(modifier = Modifier.height(10.dp))
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
progress = answerItem.progress,
strokeCap = StrokeCap.Round,
)
}
}
}

View File

@@ -20,5 +20,8 @@ enum class PollKind {
Disclosed,
/** Results should be only revealed when the poll is ended. */
Undisclosed
Undisclosed,
}
val PollKind.isDisclosed: Boolean
get() = this == PollKind.Disclosed