Update UI for poll in the timeline
This commit is contained in:
parent
3a476f5130
commit
00752d851a
7 changed files with 109 additions and 101 deletions
|
|
@ -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.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
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 io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -36,16 +36,15 @@ class TimelineItemContentPollFactory @Inject constructor(
|
||||||
if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
|
if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent
|
||||||
|
|
||||||
// Todo Move this computation to the matrix rust sdk
|
// 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 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 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 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(
|
PollAnswerItem(
|
||||||
answer = answer,
|
answer = answer,
|
||||||
isSelected = answer.id in userVotes,
|
isSelected = answer.id in userVotes,
|
||||||
isDisclosed = showResults,
|
isDisclosed = content.kind.isDisclosed,
|
||||||
votesCount = votesCount,
|
votesCount = votesCount,
|
||||||
progress = progress,
|
progress = progress,
|
||||||
)
|
)
|
||||||
|
|
@ -56,7 +55,6 @@ class TimelineItemContentPollFactory @Inject constructor(
|
||||||
answerItems = answerItems,
|
answerItems = answerItems,
|
||||||
votes = content.votes,
|
votes = content.votes,
|
||||||
pollKind = content.kind,
|
pollKind = content.kind,
|
||||||
isDisclosed = showResults
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ 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 isDisclosed: Boolean,
|
|
||||||
) : TimelineItemEventContent {
|
) : TimelineItemEventContent {
|
||||||
override val type: String = "TimelineItemPollContent"
|
override val type: String = "TimelineItemPollContent"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,13 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider<TimelineIt
|
||||||
override val values: Sequence<TimelineItemPollContent>
|
override val values: Sequence<TimelineItemPollContent>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aTimelineItemPollContent(),
|
aTimelineItemPollContent(),
|
||||||
aTimelineItemPollContent().copy(isDisclosed = true),
|
aTimelineItemPollContent().copy(pollKind = PollKind.Undisclosed),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTimelineItemPollContent(): TimelineItemPollContent {
|
fun aTimelineItemPollContent(): TimelineItemPollContent {
|
||||||
return TimelineItemPollContent(
|
return TimelineItemPollContent(
|
||||||
pollKind = PollKind.Disclosed,
|
pollKind = PollKind.Disclosed,
|
||||||
isDisclosed = false,
|
|
||||||
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(),
|
||||||
votes = emptyMap(),
|
votes = emptyMap(),
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(libs.androidx.constraintlayout)
|
|
||||||
implementation(libs.androidx.constraintlayout.compose)
|
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,14 @@ package io.element.android.features.poll.api
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
import androidx.compose.material.icons.Icons
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -47,54 +49,80 @@ fun ActivePollContentView(
|
||||||
onAnswerSelected: (PollAnswer) -> Unit,
|
onAnswerSelected: (PollAnswer) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val showResults = answerItems.any { it.isSelected }
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.selectableGroup()
|
.selectableGroup()
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
PollTitle(title = question)
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
) {
|
|
||||||
Icon(imageVector = Icons.Default.BarChart, contentDescription = null)
|
|
||||||
Text(
|
|
||||||
text = question,
|
|
||||||
style = ElementTheme.typography.fontBodyLgMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
answerItems.forEach { answerItem ->
|
PollAnswers(answerItems = answerItems, onAnswerSelected = onAnswerSelected)
|
||||||
PollAnswerView(
|
|
||||||
answerItem = answerItem,
|
|
||||||
onClick = { onAnswerSelected(answerItem.answer) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val votesCount = answerItems.sumOf { it.votesCount }
|
when (pollKind) {
|
||||||
when {
|
PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems)
|
||||||
pollKind == PollKind.Undisclosed -> {
|
PollKind.Undisclosed -> UndisclosedPollBottomNotice()
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@DayNightPreviews
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ActivePollContentNoResultsPreview() = ElementPreview {
|
internal fun ActivePollContentNoResultsPreview() = ElementPreview {
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,21 @@
|
||||||
|
|
||||||
package io.element.android.features.poll.api
|
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.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.foundation.selection.selectable
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.unit.dp
|
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.DayNightPreviews
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
|
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.theme.ElementTheme
|
||||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||||
|
|
||||||
@Suppress("DestructuringDeclarationWithTooManyEntries") // This is necessary to declare the constraints ids
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PollAnswerView(
|
fun PollAnswerView(
|
||||||
answerItem: PollAnswerItem,
|
answerItem: PollAnswerItem,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
ConstraintLayout(
|
Row(
|
||||||
modifier
|
modifier
|
||||||
.wrapContentHeight()
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.selectable(
|
.selectable(
|
||||||
selected = answerItem.isSelected,
|
selected = answerItem.isSelected,
|
||||||
|
|
@ -53,56 +54,38 @@ fun PollAnswerView(
|
||||||
role = Role.RadioButton,
|
role = Role.RadioButton,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
val (radioButton, answerText, votesText, progressBar) = createRefs()
|
|
||||||
RadioButton(
|
RadioButton(
|
||||||
modifier = Modifier.constrainAs(radioButton) {
|
modifier = Modifier.size(22.dp),
|
||||||
top.linkTo(answerText.top)
|
|
||||||
bottom.linkTo(answerText.bottom)
|
|
||||||
start.linkTo(parent.start)
|
|
||||||
end.linkTo(answerText.start)
|
|
||||||
},
|
|
||||||
selected = answerItem.isSelected,
|
selected = answerItem.isSelected,
|
||||||
onClick = null // null recommended for accessibility with screenreaders
|
onClick = null // null recommended for accessibility with screenreaders
|
||||||
)
|
)
|
||||||
Text(
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
modifier = Modifier.constrainAs(answerText) {
|
Column {
|
||||||
width = Dimension.fillToConstraints
|
Row {
|
||||||
top.linkTo(parent.top)
|
Text(
|
||||||
start.linkTo(radioButton.end, margin = 8.dp)
|
modifier = Modifier.weight(1f),
|
||||||
end.linkTo(votesText.start)
|
text = answerItem.answer.text
|
||||||
bottom.linkTo(progressBar.top)
|
)
|
||||||
},
|
if (answerItem.isDisclosed) {
|
||||||
text = answerItem.answer.text,
|
Text(
|
||||||
)
|
modifier = Modifier.align(Alignment.Bottom),
|
||||||
Text(
|
text = pluralStringResource(
|
||||||
modifier = Modifier.constrainAs(votesText) {
|
id = CommonPlurals.common_poll_votes_count,
|
||||||
start.linkTo(answerText.end)
|
count = answerItem.votesCount,
|
||||||
end.linkTo(parent.end)
|
answerItem.votesCount
|
||||||
bottom.linkTo(answerText.bottom)
|
),
|
||||||
visibility = if (answerItem.isDisclosed) Visibility.Visible else Visibility.Gone
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
},
|
color = ElementTheme.colors.textSecondary,
|
||||||
text = pluralStringResource(
|
)
|
||||||
id = CommonPlurals.common_poll_votes_count,
|
}
|
||||||
count = answerItem.votesCount,
|
}
|
||||||
answerItem.votesCount
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
),
|
LinearProgressIndicator(
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
color = ElementTheme.colors.textSecondary,
|
progress = answerItem.progress,
|
||||||
)
|
strokeCap = StrokeCap.Round,
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,5 +20,8 @@ enum class PollKind {
|
||||||
Disclosed,
|
Disclosed,
|
||||||
|
|
||||||
/** Results should be only revealed when the poll is ended. */
|
/** Results should be only revealed when the poll is ended. */
|
||||||
Undisclosed
|
Undisclosed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val PollKind.isDisclosed: Boolean
|
||||||
|
get() = this == PollKind.Disclosed
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue