First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
}
android {
namespace = "io.element.android.features.poll.api"
}
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.matrix.api)
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.actions
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.Timeline
interface EndPollAction {
suspend fun execute(timeline: Timeline, pollStartId: EventId): Result<Unit>
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.actions
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.Timeline
interface SendPollResponseAction {
suspend fun execute(
timeline: Timeline,
pollStartId: EventId,
answerId: String
): Result<Unit>
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.create
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.timeline.Timeline
interface CreatePollEntryPoint : FeatureEntryPoint {
data class Params(
val timelineMode: Timeline.Mode,
val mode: CreatePollMode,
)
fun createNode(
parentNode: Node,
buildContext: BuildContext,
params: Params,
): Node
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.create
import io.element.android.libraries.matrix.api.core.EventId
sealed interface CreatePollMode {
data object NewPoll : CreatePollMode
data class EditPoll(val eventId: EventId) : CreatePollMode
}
@@ -0,0 +1,13 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.history
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
interface PollHistoryEntryPoint : SimpleFeatureEntryPoint
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.pollcontent
import io.element.android.libraries.matrix.api.poll.PollAnswer
/**
* UI model for a [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 showVotes whether the votes for this answer should be displayed.
* @property votesCount the number of votes for this answer.
* @property percentage the percentage of votes for this answer.
*/
data class PollAnswerItem(
val answer: PollAnswer,
val isSelected: Boolean,
val isEnabled: Boolean,
val isWinner: Boolean,
val showVotes: Boolean,
val votesCount: Int,
val percentage: Float,
)
@@ -0,0 +1,204 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.pollcontent
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
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.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.poll.api.R
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
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.progressIndicatorTrackColor
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun PollAnswerView(
answerItem: PollAnswerItem,
modifier: Modifier = Modifier,
) {
val nbVotesText = pluralStringResource(
id = CommonPlurals.common_poll_votes_count,
count = answerItem.votesCount,
answerItem.votesCount,
)
val a11yText = buildString {
val sentenceDelimiter = stringResource(CommonStrings.common_sentence_delimiter)
append(answerItem.answer.text.removeSuffix("."))
if (answerItem.showVotes) {
append(sentenceDelimiter)
append(nbVotesText)
if (answerItem.votesCount != 0) {
append(sentenceDelimiter)
(answerItem.percentage * 100).toInt().let { percent ->
append(pluralStringResource(R.plurals.a11y_polls_percent_of_total, percent, percent))
}
}
if (answerItem.isWinner) {
append(sentenceDelimiter)
append(stringResource(R.string.a11y_polls_winning_answer))
}
}
}
Row(
modifier = modifier
.fillMaxWidth()
.clearAndSetSemantics {
contentDescription = a11yText
},
) {
Icon(
imageVector = if (answerItem.isSelected) {
CompoundIcons.CheckCircleSolid()
} else {
CompoundIcons.Circle()
},
contentDescription = null,
modifier = Modifier
.padding(0.5.dp)
.size(22.dp),
tint = if (answerItem.isEnabled) {
if (answerItem.isSelected) {
ElementTheme.colors.iconPrimary
} else {
ElementTheme.colors.iconSecondary
}
} else {
ElementTheme.colors.iconDisabled
},
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Row {
Text(
modifier = Modifier.weight(1f),
text = answerItem.answer.text,
style = if (answerItem.isWinner) ElementTheme.typography.fontBodyLgMedium else ElementTheme.typography.fontBodyLgRegular,
)
if (answerItem.showVotes) {
Row(
modifier = Modifier.align(Alignment.Bottom),
verticalAlignment = Alignment.CenterVertically,
) {
if (answerItem.isWinner) {
Icon(
resourceId = CommonDrawables.ic_winner,
contentDescription = null,
tint = ElementTheme.colors.iconAccentTertiary,
)
Spacer(modifier = Modifier.width(2.dp))
Text(
text = nbVotesText,
style = ElementTheme.typography.fontBodySmMedium,
color = ElementTheme.colors.textPrimary,
)
} else {
Text(
text = nbVotesText,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
}
}
}
}
Spacer(modifier = Modifier.height(10.dp))
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
color = if (answerItem.isWinner) ElementTheme.colors.textSuccessPrimary else answerItem.isEnabled.toEnabledColor(),
progress = {
when {
answerItem.showVotes -> answerItem.percentage
answerItem.isSelected -> 1f
else -> 0f
}
},
trackColor = ElementTheme.colors.progressIndicatorTrackColor,
strokeCap = StrokeCap.Round,
)
}
}
}
@PreviewsDayNight
@Composable
internal fun PollAnswerViewDisclosedNotSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(showVotes = true, isSelected = false),
)
}
@PreviewsDayNight
@Composable
internal fun PollAnswerViewDisclosedSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(showVotes = true, isSelected = true),
)
}
@PreviewsDayNight
@Composable
internal fun PollAnswerViewUndisclosedNotSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(showVotes = false, isSelected = false),
)
}
@PreviewsDayNight
@Composable
internal fun PollAnswerViewUndisclosedSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(showVotes = false, isSelected = true),
)
}
@PreviewsDayNight
@Composable
internal fun PollAnswerViewEndedWinnerNotSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(showVotes = true, isSelected = false, isEnabled = false, isWinner = true),
)
}
@PreviewsDayNight
@Composable
internal fun PollAnswerViewEndedWinnerSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(showVotes = true, isSelected = true, isEnabled = false, isWinner = true),
)
}
@PreviewsDayNight
@Composable
internal fun PollAnswerViewEndedSelectedPreview() = ElementPreview {
PollAnswerView(
answerItem = aPollAnswerItem(showVotes = true, isSelected = true, isEnabled = false, isWinner = false),
)
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.pollcontent
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollKind
import kotlinx.collections.immutable.ImmutableList
/**
* UI model for a PollContent.
* @property eventId the event id of the poll.
* @property question the poll question.
* @property answerItems the list of answers.
* @property pollKind the kind of poll.
* @property isPollEditable whether the poll is editable.
* @property isPollEnded whether the poll is ended.
* @property isMine whether the poll has been created by me.
*/
data class PollContentState(
val eventId: EventId?,
val question: String,
val answerItems: ImmutableList<PollAnswerItem>,
val pollKind: PollKind,
val isPollEditable: Boolean,
val isPollEnded: Boolean,
val isMine: Boolean,
)
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.pollcontent
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
interface PollContentStateFactory {
suspend fun create(eventTimelineItem: EventTimelineItem, content: PollContent): PollContentState {
return create(
eventId = eventTimelineItem.eventId,
isEditable = eventTimelineItem.isEditable,
isOwn = eventTimelineItem.isOwn,
content = content,
)
}
suspend fun create(eventId: EventId?, isEditable: Boolean, isOwn: Boolean, content: PollContent): PollContentState
}
@@ -0,0 +1,100 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.pollcontent
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
fun aPollQuestion() = "What type of food should we have at the party?"
fun aPollAnswerItemList(
hasVotes: Boolean = true,
isEnded: Boolean = false,
showVotes: Boolean = true,
) = persistentListOf(
aPollAnswerItem(
answer = PollAnswer("option_1", "Italian \uD83C\uDDEE\uD83C\uDDF9"),
showVotes = showVotes,
isEnabled = !isEnded,
isWinner = isEnded,
votesCount = if (hasVotes) 5 else 0,
percentage = if (hasVotes) 0.5f else 0f
),
aPollAnswerItem(
answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"),
showVotes = showVotes,
isEnabled = !isEnded,
isWinner = false,
votesCount = 0,
percentage = 0f
),
aPollAnswerItem(
answer = PollAnswer("option_3", "Brazilian \uD83C\uDDE7\uD83C\uDDF7"),
showVotes = showVotes,
isEnabled = !isEnded,
isWinner = false,
isSelected = true,
votesCount = if (hasVotes) 1 else 0,
percentage = if (hasVotes) 0.1f else 0f
),
aPollAnswerItem(
showVotes = showVotes,
isEnabled = !isEnded,
votesCount = if (hasVotes) 4 else 0,
percentage = if (hasVotes) 0.4f else 0f,
),
)
fun aPollAnswerItem(
answer: PollAnswer = PollAnswer(
"option_4",
"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,
showVotes: Boolean = true,
votesCount: Int = 4,
percentage: Float = 0.4f,
) = PollAnswerItem(
answer = answer,
isSelected = isSelected,
isEnabled = isEnabled,
isWinner = isWinner,
showVotes = showVotes,
votesCount = votesCount,
percentage = percentage
)
fun aPollContentState(
eventId: EventId? = null,
isMine: Boolean = false,
isEnded: Boolean = false,
showVotes: Boolean = true,
isPollEditable: Boolean = true,
hasVotes: Boolean = true,
question: String = aPollQuestion(),
pollKind: PollKind = PollKind.Disclosed,
answerItems: ImmutableList<PollAnswerItem> = aPollAnswerItemList(
isEnded = isEnded,
showVotes = showVotes,
hasVotes = hasVotes
),
) = PollContentState(
eventId = eventId,
question = question,
answerItems = answerItems,
pollKind = pollKind,
isPollEditable = isMine && !isEnded && isPollEditable,
isPollEnded = isEnded,
isMine = isMine,
)
@@ -0,0 +1,302 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.pollcontent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
@Composable
fun PollContentView(
state: PollContentState,
onSelectAnswer: (pollStartId: EventId, answerId: String) -> Unit,
onEditPoll: (pollStartId: EventId) -> Unit,
onEndPoll: (pollStartId: EventId) -> Unit,
modifier: Modifier = Modifier,
) {
PollContentView(
eventId = state.eventId,
question = state.question,
answerItems = state.answerItems,
pollKind = state.pollKind,
isPollEditable = state.isPollEditable,
isPollEnded = state.isPollEnded,
isMine = state.isMine,
onEditPoll = onEditPoll,
onSelectAnswer = onSelectAnswer,
onEndPoll = onEndPoll,
modifier = modifier,
)
}
@Composable
fun PollContentView(
eventId: EventId?,
question: String,
answerItems: ImmutableList<PollAnswerItem>,
pollKind: PollKind,
isPollEditable: Boolean,
isPollEnded: Boolean,
isMine: Boolean,
onSelectAnswer: (pollStartId: EventId, answerId: String) -> Unit,
onEditPoll: (pollStartId: EventId) -> Unit,
onEndPoll: (pollStartId: EventId) -> Unit,
modifier: Modifier = Modifier,
) {
val votesCount = remember(answerItems) { answerItems.sumOf { it.votesCount } }
fun onSelectAnswer(pollAnswer: PollAnswer) {
eventId?.let { onSelectAnswer(it, pollAnswer.id) }
}
fun onEditPoll() {
eventId?.let { onEditPoll(it) }
}
fun onEndPoll() {
eventId?.let { onEndPoll(it) }
}
var showConfirmation: Boolean by remember { mutableStateOf(false) }
if (showConfirmation) {
ConfirmationDialog(
content = stringResource(id = CommonStrings.common_poll_end_confirmation),
onSubmitClick = {
onEndPoll()
showConfirmation = false
},
onDismiss = { showConfirmation = false },
)
}
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
PollTitleView(title = question, isPollEnded = isPollEnded)
PollAnswers(answerItems = answerItems, onSelectAnswer = ::onSelectAnswer)
if (isPollEnded || pollKind == PollKind.Disclosed) {
DisclosedPollBottomNotice(votesCount = votesCount)
} else {
UndisclosedPollBottomNotice()
}
if (isMine) {
CreatorView(
isPollEnded = isPollEnded,
isPollEditable = isPollEditable,
onEditPoll = ::onEditPoll,
onEndPoll = { showConfirmation = true },
modifier = Modifier.fillMaxWidth(),
)
}
}
}
@Composable
private fun PollAnswers(
answerItems: ImmutableList<PollAnswerItem>,
onSelectAnswer: (PollAnswer) -> Unit,
) {
Column(
modifier = Modifier.selectableGroup(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
answerItems.forEach {
PollAnswerView(
answerItem = it,
modifier = Modifier
.selectable(
selected = it.isSelected,
enabled = it.isEnabled,
onClick = { onSelectAnswer(it.answer) },
role = Role.RadioButton,
),
)
}
}
}
@Composable
private fun ColumnScope.DisclosedPollBottomNotice(
votesCount: Int,
) {
Text(
modifier = Modifier.align(Alignment.End),
style = ElementTheme.typography.fontBodyXsRegular,
color = ElementTheme.colors.textSecondary,
text = stringResource(CommonStrings.common_poll_total_votes, votesCount),
)
}
@Composable
private 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),
)
}
@Composable
private fun CreatorView(
isPollEnded: Boolean,
isPollEditable: Boolean,
onEditPoll: () -> Unit,
onEndPoll: () -> Unit,
modifier: Modifier = Modifier
) {
when {
isPollEditable ->
Button(
text = stringResource(id = CommonStrings.action_edit_poll),
onClick = onEditPoll,
modifier = modifier,
)
!isPollEnded ->
Button(
text = stringResource(id = CommonStrings.action_end_poll),
onClick = onEndPoll,
modifier = modifier,
)
}
}
@PreviewsDayNight
@Composable
internal fun PollContentViewUndisclosedPreview() = ElementPreview {
PollContentView(
eventId = EventId("\$anEventId"),
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(showVotes = false),
pollKind = PollKind.Undisclosed,
isPollEnded = false,
isPollEditable = false,
isMine = false,
onSelectAnswer = { _, _ -> },
onEditPoll = {},
onEndPoll = {},
)
}
@PreviewsDayNight
@Composable
internal fun PollContentViewDisclosedPreview() = ElementPreview {
PollContentView(
eventId = EventId("\$anEventId"),
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(),
pollKind = PollKind.Disclosed,
isPollEnded = false,
isPollEditable = false,
isMine = false,
onSelectAnswer = { _, _ -> },
onEditPoll = {},
onEndPoll = {},
)
}
@PreviewsDayNight
@Composable
internal fun PollContentViewEndedPreview() = ElementPreview {
PollContentView(
eventId = EventId("\$anEventId"),
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(isEnded = true),
pollKind = PollKind.Disclosed,
isPollEnded = true,
isPollEditable = false,
isMine = false,
onSelectAnswer = { _, _ -> },
onEditPoll = {},
onEndPoll = {},
)
}
@PreviewsDayNight
@Composable
internal fun PollContentViewCreatorEditablePreview() = ElementPreview {
PollContentView(
eventId = EventId("\$anEventId"),
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(hasVotes = false, isEnded = false),
pollKind = PollKind.Disclosed,
isPollEnded = false,
isPollEditable = true,
isMine = true,
onSelectAnswer = { _, _ -> },
onEditPoll = {},
onEndPoll = {},
)
}
@PreviewsDayNight
@Composable
internal fun PollContentViewCreatorPreview() = ElementPreview {
PollContentView(
eventId = EventId("\$anEventId"),
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(isEnded = false),
pollKind = PollKind.Disclosed,
isPollEnded = false,
isPollEditable = false,
isMine = true,
onSelectAnswer = { _, _ -> },
onEditPoll = {},
onEndPoll = {},
)
}
@PreviewsDayNight
@Composable
internal fun PollContentViewCreatorEndedPreview() = ElementPreview {
PollContentView(
eventId = EventId("\$anEventId"),
question = "What type of food should we have at the party?",
answerItems = aPollAnswerItemList(isEnded = true),
pollKind = PollKind.Disclosed,
isPollEnded = true,
isPollEditable = false,
isMine = true,
onSelectAnswer = { _, _ -> },
onEditPoll = {},
onEndPoll = {},
)
}
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.api.pollcontent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun PollTitleView(
title: String,
isPollEnded: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
if (isPollEnded) {
Icon(
imageVector = CompoundIcons.PollsEnd(),
contentDescription = stringResource(id = CommonStrings.a11y_poll_end),
modifier = Modifier.size(22.dp)
)
} else {
Icon(
imageVector = CompoundIcons.Polls(),
contentDescription = stringResource(id = CommonStrings.a11y_poll),
modifier = Modifier.size(22.dp)
)
}
Text(
text = title,
style = ElementTheme.typography.fontBodyLgMedium
)
}
}
@PreviewsDayNight
@Composable
internal fun PollTitleViewPreview() = ElementPreview {
PollTitleView(
title = "What is your favorite color?",
isPollEnded = false
)
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d procento z celkového počtu hlasů"</item>
<item quantity="few">"%1$d procenta z celkového počtu hlasů"</item>
<item quantity="other">"%1$d procent z celkového počtu hlasů"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Odstraní předchozí výběr"</string>
<string name="a11y_polls_winning_answer">"Toto je vítězná odpověď"</string>
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="zero">"%1$d y cant o\'r holl bleidleisiau"</item>
<item quantity="one">"%1$d y cant o\'r holl bleidleisiau"</item>
<item quantity="two">"%1$d y cant o\'r holl bleidleisiau"</item>
<item quantity="few">"%1$d y cant o\'r holl bleidleisiau"</item>
<item quantity="many">"%1$d y cant o\'r holl bleidleisiau"</item>
<item quantity="other">"%1$d y cant o\'r holl bleidleisiau"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Bydd yn dileu\'r dewis blaenorol"</string>
<string name="a11y_polls_winning_answer">"Dyma\'r ateb buddugol"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d% af de samlede stemmer"</item>
<item quantity="other">"%1$d procent af det samlede antal stemmer"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Fjerner tidligere valg"</string>
<string name="a11y_polls_winning_answer">"Dette er det vindende svar"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d Prozent aller Stimmen"</item>
<item quantity="other">"%1$d Prozent aller Stimmen"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Entfernt die vorherige Auswahl"</string>
<string name="a11y_polls_winning_answer">"Das ist die meistgewählte Antwort"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d τοις εκατό των συνολικών ψήφων"</item>
<item quantity="other">"%1$d τοις εκατό του συνόλου των ψήφων"</item>
</plurals>
<string name="a11y_polls_winning_answer">"Αυτή είναι η νικητήρια απάντηση"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d protsent kõikidest antud häältest"</item>
<item quantity="other">"%1$d protsenti kõikidest antud häältest"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"See kustutab eelmise valiku"</string>
<string name="a11y_polls_winning_answer">"See vastus võitis"</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_polls_will_remove_selection">"Aurreko hautaketa kenduko du"</string>
<string name="a11y_polls_winning_answer">"Erantzun hau gailendu da"</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_polls_will_remove_selection">"گزینش پیشین را برخواهد داشت"</string>
<string name="a11y_polls_winning_answer">"این پاسخ برنده است"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d prosentti kaikista äänistä"</item>
<item quantity="other">"%1$d prosenttia kaikista äänistä"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Poistaa edellisen valinnan"</string>
<string name="a11y_polls_winning_answer">"Tämä on voittava vastaus"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d pour cent du total des votes"</item>
<item quantity="other">"%1$d pour cent du total des votes"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Supprimera la sélection précédente"</string>
<string name="a11y_polls_winning_answer">"Cest la réponse gagnante"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"az összes szavazat %1$d százaléka"</item>
<item quantity="other">"az összes szavazat %1$d százaléka"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Eltávolítja a korábbi kijelölést"</string>
<string name="a11y_polls_winning_answer">"Ez a győztes válasz"</string>
</resources>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="other">"%1$d persen dari total suara"</item>
</plurals>
<string name="a11y_polls_winning_answer">"Ini adalah jawaban yang menang"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d percento dei voti totali"</item>
<item quantity="other">"%1$d percento dei voti totali"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Rimuoverà la selezione precedente"</string>
<string name="a11y_polls_winning_answer">"Questa è la risposta vincente"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="other">"%1$d 총 투표율"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"이전 선택 항목을 제거합니다"</string>
<string name="a11y_polls_winning_answer">"이것이 승리의 답입니다"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d prosent av totalt antall stemmer"</item>
<item quantity="other">"%1$d prosent av totalt antall stemmer"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Vil fjerne forrige valg"</string>
<string name="a11y_polls_winning_answer">"Dette er vinnersvaret"</string>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a11y_polls_will_remove_selection">"Verwijdert de vorige selectie"</string>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d procent wszystkich głosów"</item>
<item quantity="few">"%1$d procenty wszystkich głosów"</item>
<item quantity="many">"%1$d procent wszystkich głosów"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Spowoduje to usunięcie poprzedniego zaznaczenia"</string>
<string name="a11y_polls_winning_answer">"Zwycięska odpowiedź"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d por cento de todos os votos"</item>
<item quantity="other">"%1$d por cento de todos os votos"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Removerá a seleção anterior"</string>
<string name="a11y_polls_winning_answer">"Esta é a resposta vencedora"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d porcento de todos os votos"</item>
<item quantity="other">"%1$d porcento de todos os votos"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Irá remover seleção anterior"</string>
<string name="a11y_polls_winning_answer">"Esta é a reposta vencedora"</string>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d la suta din totalul voturilor"</item>
<item quantity="few">"%1$d la suta din totalul voturilor"</item>
<item quantity="other">"%1$d la suta din totalul voturilor"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Va șterge selecția anterioară"</string>
<string name="a11y_polls_winning_answer">"Acesta este votul câștigător"</string>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d процент от общего числа голосов"</item>
<item quantity="few">"%1$d процента от общего числа голосов"</item>
<item quantity="many">"%1$d процентов от общего числа голосов"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Удалить предыдущий ответ"</string>
<string name="a11y_polls_winning_answer">"Это лучший ответ"</string>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d percento z celkového počtu hlasov"</item>
<item quantity="few">"%1$d percentá z celkového počtu hlasov"</item>
<item quantity="other">"%1$d percent z celkového počtu hlasov"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Odstráni predchádzajúci výber"</string>
<string name="a11y_polls_winning_answer">"Toto je víťazná odpoveď"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d procent av totala röster"</item>
<item quantity="other">"%1$d procent av totala röster"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Kommer att ta bort föregående val"</string>
<string name="a11y_polls_winning_answer">"Detta är det vinnande svaret"</string>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d відсоток від усіх голосів"</item>
<item quantity="few">"%1$d відсотки від усіх голосів"</item>
<item quantity="many">"%1$d відсотків від усіх голосів"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Попередній вибір буде прибрано"</string>
<string name="a11y_polls_winning_answer">"Ця відповідь перемогла"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"Jami ovozlarning %1$d foizi"</item>
<item quantity="other">"Jami ovozlarning %1$d foizi"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Oldingi tanlov olib tashlanadi"</string>
<string name="a11y_polls_winning_answer">"Bu g\'alaba qozongan javob"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="other">"總票數的百分之 %1$d"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"將會移除先前的選擇"</string>
<string name="a11y_polls_winning_answer">"這是得票數最高的選項"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="other">"%1$d 总投票百分比"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"将移除之前的选择"</string>
<string name="a11y_polls_winning_answer">"这是获胜的答案"</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="a11y_polls_percent_of_total">
<item quantity="one">"%1$d percent of total votes"</item>
<item quantity="other">"%1$d percents of total votes"</item>
</plurals>
<string name="a11y_polls_will_remove_selection">"Will remove previous selection"</string>
<string name="a11y_polls_winning_answer">"This is the winning answer"</string>
</resources>
+46
View File
@@ -0,0 +1,46 @@
import extension.setupDependencyInjection
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.poll.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupDependencyInjection()
dependencies {
api(projects.features.poll.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.services.analytics.api)
implementation(projects.features.messages.api)
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.uiStrings)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.analytics.test)
testImplementation(projects.features.messages.test)
testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.features.poll.test)
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl
internal object PollConstants {
const val MIN_ANSWERS = 2
const val MAX_ANSWERS = 20
const val MAX_ANSWER_LENGTH = 240
const val MAX_SELECTIONS = 1
}
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.actions
import dev.zacsweers.metro.ContributesBinding
import im.vector.app.features.analytics.plan.PollEnd
import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.services.analytics.api.AnalyticsService
@ContributesBinding(RoomScope::class)
class DefaultEndPollAction(
private val analyticsService: AnalyticsService,
) : EndPollAction {
override suspend fun execute(timeline: Timeline, pollStartId: EventId): Result<Unit> {
return timeline.endPoll(
pollStartId = pollStartId,
text = "The poll with event id: $pollStartId has ended."
).onSuccess {
analyticsService.capture(PollEnd())
}
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.actions
import dev.zacsweers.metro.ContributesBinding
import im.vector.app.features.analytics.plan.PollVote
import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.services.analytics.api.AnalyticsService
@ContributesBinding(RoomScope::class)
class DefaultSendPollResponseAction(
private val analyticsService: AnalyticsService,
) : SendPollResponseAction {
override suspend fun execute(timeline: Timeline, pollStartId: EventId, answerId: String): Result<Unit> {
return timeline.sendPollResponse(
pollStartId = pollStartId,
answers = listOf(answerId),
).onSuccess {
analyticsService.capture(PollVote())
}
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.create
import io.element.android.libraries.matrix.api.poll.PollKind
sealed interface CreatePollEvents {
data object Save : CreatePollEvents
data class Delete(val confirmed: Boolean) : CreatePollEvents
data class SetQuestion(val question: String) : CreatePollEvents
data class SetAnswer(val index: Int, val text: String) : CreatePollEvents
data object AddAnswer : CreatePollEvents
data class RemoveAnswer(val index: Int) : CreatePollEvents
data class SetPollKind(val pollKind: PollKind) : CreatePollEvents
data object NavBack : CreatePollEvents
data object ConfirmNavBack : CreatePollEvents
data object HideConfirmation : CreatePollEvents
}
@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.create
internal sealed class CreatePollException : Exception() {
data class GetPollFailed(
override val message: String?,
override val cause: Throwable?
) : CreatePollException()
data class SavePollFailed(
override val message: String?,
override val cause: Throwable?
) : CreatePollException()
}
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.create
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.MobileScreen
import io.element.android.annotations.ContributesNode
import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.services.analytics.api.AnalyticsService
import java.util.concurrent.atomic.AtomicBoolean
@ContributesNode(RoomScope::class)
@AssistedInject
class CreatePollNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: CreatePollPresenter.Factory,
analyticsService: AnalyticsService,
) : Node(buildContext, plugins = plugins) {
data class Inputs(val mode: CreatePollMode, val timelineMode: Timeline.Mode) : NodeInputs
private val inputs: Inputs = inputs()
private var isNavigatingUp = AtomicBoolean(false)
private val presenter = presenterFactory.create(
backNavigator = {
if (isNavigatingUp.compareAndSet(false, true)) {
navigateUp()
}
},
mode = inputs.mode,
timelineMode = inputs.timelineMode,
)
init {
lifecycle.subscribe(
onResume = {
analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.CreatePollView))
}
)
}
@Composable
override fun View(modifier: Modifier) {
CreatePollView(
state = presenter.present(),
modifier = modifier,
)
}
}
@@ -0,0 +1,244 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.create
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.Composer
import im.vector.app.features.analytics.plan.PollCreation
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.features.poll.impl.PollConstants.MAX_SELECTIONS
import io.element.android.features.poll.impl.data.PollRepository
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.poll.PollAnswer
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.Timeline
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
import timber.log.Timber
@AssistedInject
class CreatePollPresenter(
repositoryFactory: PollRepository.Factory,
private val analyticsService: AnalyticsService,
private val messageComposerContext: MessageComposerContext,
@Assisted private val navigateUp: () -> Unit,
@Assisted private val mode: CreatePollMode,
@Assisted private val timelineMode: Timeline.Mode,
) : Presenter<CreatePollState> {
@AssistedFactory
fun interface Factory {
fun create(
timelineMode: Timeline.Mode,
backNavigator: () -> Unit,
mode: CreatePollMode
): CreatePollPresenter
}
private val repository = repositoryFactory.create(timelineMode)
@Composable
override fun present(): CreatePollState {
// The initial state of the form. In edit mode this will be populated with the poll being edited.
var initialPoll: PollFormState by rememberSaveable(stateSaver = pollFormStateSaver) {
mutableStateOf(PollFormState.Empty)
}
// The current state of the form.
var poll: PollFormState by rememberSaveable(stateSaver = pollFormStateSaver) {
mutableStateOf(initialPoll)
}
// Whether the form has been changed from the initial state
val isDirty: Boolean by remember { derivedStateOf { poll != initialPoll } }
var showBackConfirmation: Boolean by rememberSaveable { mutableStateOf(false) }
var showDeleteConfirmation: Boolean by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (mode is CreatePollMode.EditPoll) {
repository.getPoll(mode.eventId).onSuccess {
val loadedPoll = PollFormState(
question = it.question,
answers = it.answers.map(PollAnswer::text).toImmutableList(),
isDisclosed = it.kind.isDisclosed,
)
initialPoll = loadedPoll
poll = loadedPoll
}.onFailure {
analyticsService.trackGetPollFailed(it)
navigateUp()
}
}
}
val canSave: Boolean by remember { derivedStateOf { poll.isValid } }
val canAddAnswer: Boolean by remember { derivedStateOf { poll.canAddAnswer } }
val immutableAnswers: ImmutableList<Answer> by remember { derivedStateOf { poll.toUiAnswers() } }
val scope = rememberCoroutineScope()
fun handleEvent(event: CreatePollEvents) {
when (event) {
is CreatePollEvents.Save -> scope.launch {
if (canSave) {
repository.savePoll(
existingPollId = when (mode) {
is CreatePollMode.EditPoll -> mode.eventId
is CreatePollMode.NewPoll -> null
},
question = poll.question,
answers = poll.answers,
pollKind = poll.pollKind,
maxSelections = MAX_SELECTIONS,
).onSuccess {
analyticsService.capturePollSaved(
isUndisclosed = poll.pollKind == PollKind.Undisclosed,
numberOfAnswers = poll.answers.size,
)
}.onFailure {
analyticsService.trackSavePollFailed(it, mode)
}
navigateUp()
} else {
Timber.d("Cannot create poll")
}
}
is CreatePollEvents.Delete -> {
if (mode !is CreatePollMode.EditPoll) {
return
}
if (!event.confirmed) {
showDeleteConfirmation = true
return
}
scope.launch {
showDeleteConfirmation = false
repository.deletePoll(mode.eventId)
navigateUp()
}
}
is CreatePollEvents.AddAnswer -> {
poll = poll.withNewAnswer()
}
is CreatePollEvents.RemoveAnswer -> {
poll = poll.withAnswerRemoved(event.index)
}
is CreatePollEvents.SetAnswer -> {
poll = poll.withAnswerChanged(event.index, event.text)
}
is CreatePollEvents.SetPollKind -> {
poll = poll.copy(isDisclosed = event.pollKind.isDisclosed)
}
is CreatePollEvents.SetQuestion -> {
poll = poll.copy(question = event.question)
}
is CreatePollEvents.NavBack -> {
navigateUp()
}
CreatePollEvents.ConfirmNavBack -> {
val shouldConfirm = isDirty
if (shouldConfirm) {
showBackConfirmation = true
} else {
navigateUp()
}
}
is CreatePollEvents.HideConfirmation -> {
showBackConfirmation = false
showDeleteConfirmation = false
}
}
}
return CreatePollState(
mode = when (mode) {
is CreatePollMode.NewPoll -> CreatePollState.Mode.New
is CreatePollMode.EditPoll -> CreatePollState.Mode.Edit
},
canSave = canSave,
canAddAnswer = canAddAnswer,
question = poll.question,
answers = immutableAnswers,
pollKind = poll.pollKind,
showBackConfirmation = showBackConfirmation,
showDeleteConfirmation = showDeleteConfirmation,
eventSink = ::handleEvent,
)
}
private fun AnalyticsService.capturePollSaved(
isUndisclosed: Boolean,
numberOfAnswers: Int,
) {
capture(
Composer(
inThread = messageComposerContext.composerMode.inThread,
isEditing = mode is CreatePollMode.EditPoll,
isReply = messageComposerContext.composerMode.isReply,
messageType = Composer.MessageType.Poll,
)
)
capture(
PollCreation(
action = when (mode) {
is CreatePollMode.EditPoll -> PollCreation.Action.Edit
is CreatePollMode.NewPoll -> PollCreation.Action.Create
},
isUndisclosed = isUndisclosed,
numberOfAnswers = numberOfAnswers,
)
)
}
}
private fun AnalyticsService.trackGetPollFailed(cause: Throwable) {
val exception = CreatePollException.GetPollFailed(
message = "Tried to edit poll but couldn't get poll",
cause = cause,
)
Timber.e(exception)
trackError(exception)
}
private fun AnalyticsService.trackSavePollFailed(cause: Throwable, mode: CreatePollMode) {
val exception = CreatePollException.SavePollFailed(
message = when (mode) {
CreatePollMode.NewPoll -> "Failed to create poll"
is CreatePollMode.EditPoll -> "Failed to edit poll"
},
cause = cause,
)
Timber.e(exception)
trackError(exception)
}
fun PollFormState.toUiAnswers(): ImmutableList<Answer> {
return answers.map { answer ->
Answer(
text = answer,
canDelete = canDeleteAnswer,
)
}.toImmutableList()
}
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.create
import io.element.android.libraries.matrix.api.poll.PollKind
import kotlinx.collections.immutable.ImmutableList
data class CreatePollState(
val mode: Mode,
val canSave: Boolean,
val canAddAnswer: Boolean,
val question: String,
val answers: ImmutableList<Answer>,
val pollKind: PollKind,
val showBackConfirmation: Boolean,
val showDeleteConfirmation: Boolean,
val eventSink: (CreatePollEvents) -> Unit,
) {
enum class Mode {
New,
Edit,
}
val canDelete: Boolean = mode == Mode.Edit
}
data class Answer(
val text: String,
val canDelete: Boolean,
)
@@ -0,0 +1,177 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.create
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.poll.PollKind
import kotlinx.collections.immutable.toImmutableList
class CreatePollStateProvider : PreviewParameterProvider<CreatePollState> {
override val values: Sequence<CreatePollState>
get() = sequenceOf(
aCreatePollState(
mode = CreatePollState.Mode.New,
canCreate = false,
canAddAnswer = true,
question = "",
answers = listOf(
Answer("", false),
Answer("", false)
),
pollKind = PollKind.Disclosed,
showBackConfirmation = false,
showDeleteConfirmation = false,
),
aCreatePollState(
mode = CreatePollState.Mode.New,
canCreate = true,
canAddAnswer = true,
question = "What type of food should we have?",
answers = listOf(
Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", false),
Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", false),
),
showBackConfirmation = false,
showDeleteConfirmation = false,
pollKind = PollKind.Undisclosed,
),
aCreatePollState(
mode = CreatePollState.Mode.New,
canCreate = true,
canAddAnswer = true,
question = "What type of food should we have?",
answers = listOf(
Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", false),
Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", false),
),
showBackConfirmation = true,
showDeleteConfirmation = false,
pollKind = PollKind.Undisclosed,
),
aCreatePollState(
mode = CreatePollState.Mode.New,
canCreate = true,
canAddAnswer = true,
question = "What type of food should we have?",
answers = listOf(
Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", true),
Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", true),
Answer("Brazilian \uD83C\uDDE7\uD83C\uDDF7", true),
Answer("French \uD83C\uDDEB\uD83C\uDDF7", true),
),
showBackConfirmation = false,
showDeleteConfirmation = false,
pollKind = PollKind.Undisclosed,
),
aCreatePollState(
mode = CreatePollState.Mode.New,
canCreate = true,
canAddAnswer = false,
question = "Should there be more than 20 answers?",
answers = listOf(
Answer("1", true),
Answer("2", true),
Answer("3", true),
Answer("4", true),
Answer("5", true),
Answer("6", true),
Answer("7", true),
Answer("8", true),
Answer("9", true),
Answer("10", true),
Answer("11", true),
Answer("12", true),
Answer("13", true),
Answer("14", true),
Answer("15", true),
Answer("16", true),
Answer("17", true),
Answer("18", true),
Answer("19", true),
Answer("20", true),
),
showBackConfirmation = false,
showDeleteConfirmation = false,
pollKind = PollKind.Undisclosed,
),
aCreatePollState(
mode = CreatePollState.Mode.New,
canCreate = true,
canAddAnswer = true,
question = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." +
" Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor" +
" in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt" +
" in culpa qui officia deserunt mollit anim id est laborum.",
answers = listOf(
Answer(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." +
" Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis a.",
false
),
Answer(
"Laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore" +
" eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mol.",
false
),
),
showBackConfirmation = false,
showDeleteConfirmation = false,
pollKind = PollKind.Undisclosed,
),
aCreatePollState(
mode = CreatePollState.Mode.Edit,
canCreate = false,
canAddAnswer = true,
question = "",
answers = listOf(
Answer("", false),
Answer("", false)
),
pollKind = PollKind.Disclosed,
showDeleteConfirmation = false,
showBackConfirmation = false,
),
aCreatePollState(
mode = CreatePollState.Mode.Edit,
canCreate = false,
canAddAnswer = true,
question = "",
answers = listOf(
Answer("", false),
Answer("", false)
),
pollKind = PollKind.Disclosed,
showDeleteConfirmation = true,
showBackConfirmation = false,
),
)
}
private fun aCreatePollState(
mode: CreatePollState.Mode,
canCreate: Boolean,
canAddAnswer: Boolean,
question: String,
answers: List<Answer>,
showBackConfirmation: Boolean,
showDeleteConfirmation: Boolean,
pollKind: PollKind
): CreatePollState {
return CreatePollState(
mode = mode,
canSave = canCreate,
canAddAnswer = canAddAnswer,
question = question,
answers = answers.toImmutableList(),
showBackConfirmation = showBackConfirmation,
showDeleteConfirmation = showDeleteConfirmation,
pollKind = pollKind,
eventSink = {}
)
}
@@ -0,0 +1,244 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.create
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.poll.impl.R
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.SaveChangesDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListItemStyle
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun CreatePollView(
state: CreatePollState,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) }
BackHandler(onBack = navBack)
if (state.showBackConfirmation) {
SaveChangesDialog(
onSubmitClick = { state.eventSink(CreatePollEvents.NavBack) },
onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) }
)
}
if (state.showDeleteConfirmation) {
ConfirmationDialog(
title = stringResource(id = R.string.screen_edit_poll_delete_confirmation_title),
content = stringResource(id = R.string.screen_edit_poll_delete_confirmation),
onSubmitClick = { state.eventSink(CreatePollEvents.Delete(confirmed = true)) },
onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) }
)
}
val questionFocusRequester = remember { FocusRequester() }
val answerFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
questionFocusRequester.requestFocus()
}
Scaffold(
modifier = modifier,
topBar = {
CreatePollTopAppBar(
mode = state.mode,
saveEnabled = state.canSave,
onBackClick = navBack,
onSaveClick = { state.eventSink(CreatePollEvents.Save) }
)
},
) { paddingValues ->
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
.imePadding()
.fillMaxSize(),
state = lazyListState,
) {
item {
Column {
ListItem(
headlineContent = {
TextField(
label = stringResource(id = R.string.screen_create_poll_question_desc),
value = state.question,
onValueChange = {
state.eventSink(CreatePollEvents.SetQuestion(it))
},
modifier = Modifier
.focusRequester(questionFocusRequester)
.fillMaxWidth(),
placeholder = stringResource(id = R.string.screen_create_poll_question_hint),
keyboardOptions = keyboardOptions,
)
}
)
}
}
itemsIndexed(state.answers) { index, answer ->
val isLastItem = index == state.answers.size - 1
ListItem(
headlineContent = {
TextField(
value = answer.text,
onValueChange = {
state.eventSink(CreatePollEvents.SetAnswer(index, it))
},
modifier = Modifier
.then(if (isLastItem) Modifier.focusRequester(answerFocusRequester) else Modifier)
.fillMaxWidth(),
placeholder = stringResource(id = R.string.screen_create_poll_answer_hint, index + 1),
keyboardOptions = keyboardOptions,
)
},
trailingContent = ListItemContent.Custom {
Icon(
imageVector = CompoundIcons.Delete(),
contentDescription = stringResource(R.string.screen_create_poll_delete_option_a11y, answer.text),
modifier = Modifier.clickable(answer.canDelete) {
state.eventSink(CreatePollEvents.RemoveAnswer(index))
},
)
},
style = if (answer.canDelete) ListItemStyle.Destructive else ListItemStyle.Default,
)
}
if (state.canAddAnswer) {
item {
ListItem(
headlineContent = { Text(text = stringResource(id = R.string.screen_create_poll_add_option_btn)) },
leadingContent = ListItemContent.Icon(
iconSource = IconSource.Vector(CompoundIcons.Plus()),
),
style = ListItemStyle.Primary,
onClick = {
state.eventSink(CreatePollEvents.AddAnswer)
coroutineScope.launch(Dispatchers.Main) {
lazyListState.animateScrollToItem(state.answers.size + 1)
answerFocusRequester.requestFocus()
}
},
)
}
}
item {
Column {
HorizontalDivider()
ListItem(
headlineContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_headline)) },
supportingContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_desc)) },
trailingContent = ListItemContent.Switch(
checked = state.pollKind == PollKind.Undisclosed,
),
onClick = {
state.eventSink(
CreatePollEvents.SetPollKind(
if (state.pollKind == PollKind.Disclosed) PollKind.Undisclosed else PollKind.Disclosed
)
)
},
)
if (state.canDelete) {
ListItem(
headlineContent = { Text(text = stringResource(id = CommonStrings.action_delete_poll)) },
style = ListItemStyle.Destructive,
onClick = { state.eventSink(CreatePollEvents.Delete(confirmed = false)) },
)
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CreatePollTopAppBar(
mode: CreatePollState.Mode,
saveEnabled: Boolean,
onBackClick: () -> Unit = {},
onSaveClick: () -> Unit = {},
) {
TopAppBar(
titleStr = when (mode) {
CreatePollState.Mode.New -> stringResource(id = R.string.screen_create_poll_title)
CreatePollState.Mode.Edit -> stringResource(id = R.string.screen_edit_poll_title)
},
navigationIcon = {
BackButton(onClick = onBackClick)
},
actions = {
TextButton(
text = when (mode) {
CreatePollState.Mode.New -> stringResource(id = CommonStrings.action_create)
CreatePollState.Mode.Edit -> stringResource(id = CommonStrings.action_done)
},
onClick = onSaveClick,
enabled = saveEnabled,
)
}
)
}
@PreviewsDayNight
@Composable
internal fun CreatePollViewPreview(
@PreviewParameter(CreatePollStateProvider::class) state: CreatePollState
) = ElementPreview {
CreatePollView(
state = state,
)
}
private val keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
imeAction = ImeAction.Next,
)
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.create
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.poll.api.create.CreatePollEntryPoint
import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class)
class DefaultCreatePollEntryPoint : CreatePollEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
params: CreatePollEntryPoint.Params,
): Node {
return parentNode.createNode<CreatePollNode>(
buildContext = buildContext,
plugins = listOf(CreatePollNode.Inputs(timelineMode = params.timelineMode, mode = params.mode))
)
}
}
@@ -0,0 +1,122 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.create
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.mapSaver
import io.element.android.features.poll.impl.PollConstants
import io.element.android.features.poll.impl.PollConstants.MIN_ANSWERS
import io.element.android.libraries.matrix.api.poll.PollKind
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
/**
* Represents the state of the poll creation / edit form.
*
* Save this state using [pollFormStateSaver].
*/
data class PollFormState(
val question: String,
val answers: ImmutableList<String>,
val isDisclosed: Boolean,
) {
companion object {
val Empty = PollFormState(
question = "",
answers = MutableList(MIN_ANSWERS) { "" }.toImmutableList(),
isDisclosed = true,
)
}
val pollKind
get() = when (isDisclosed) {
true -> PollKind.Disclosed
false -> PollKind.Undisclosed
}
/**
* Create a copy of the [PollFormState] with a new blank answer added.
*
* If the maximum number of answers has already been reached an answer is not added.
*/
fun withNewAnswer(): PollFormState {
if (!canAddAnswer) {
return this
}
return copy(answers = (answers + "").toImmutableList())
}
/**
* Create a copy of the [PollFormState] with the answer at [index] removed.
*
* If the answer doesn't exist or can't be removed, the state is unchanged.
*
* @param index the index of the answer to remove.
*
* @return a new [PollFormState] with the answer at [index] removed.
*/
fun withAnswerRemoved(index: Int): PollFormState {
if (!canDeleteAnswer) {
return this
}
return copy(answers = answers.filterIndexed { i, _ -> i != index }.toImmutableList())
}
/**
* Create a copy of the [PollFormState] with the answer at [index] changed.
*
* If the new answer is longer than [PollConstants.MAX_ANSWER_LENGTH], it will be truncated.
*
* @param index the index of the answer to change.
* @param rawAnswer the new answer as the user typed it.
*
* @return a new [PollFormState] with the answer at [index] changed.
*/
fun withAnswerChanged(index: Int, rawAnswer: String): PollFormState =
copy(answers = answers.toMutableList().apply {
this[index] = rawAnswer.take(PollConstants.MAX_ANSWER_LENGTH)
}.toImmutableList())
/**
* Whether a new answer can be added.
*/
val canAddAnswer get() = answers.size < PollConstants.MAX_ANSWERS
/**
* Whether any answer can be deleted.
*/
val canDeleteAnswer get() = answers.size > MIN_ANSWERS
/**
* Whether the form is currently valid.
*/
val isValid get() = question.isNotBlank() && answers.size >= MIN_ANSWERS && answers.all { it.isNotBlank() }
}
/**
* A [Saver] for [PollFormState].
*/
internal val pollFormStateSaver = mapSaver(
save = {
mutableMapOf(
"question" to it.question,
"answers" to it.answers.toTypedArray(),
"isDisclosed" to it.isDisclosed,
)
},
restore = { saved ->
PollFormState(
question = saved["question"] as String,
answers = (saved["answers"] as Array<*>).map { it as String }.toImmutableList(),
isDisclosed = saved["isDisclosed"] as Boolean,
)
}
)
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.data
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
@AssistedInject
class PollRepository(
private val room: JoinedRoom,
private val defaultTimelineProvider: TimelineProvider,
@Assisted private val timelineMode: Timeline.Mode,
) {
@AssistedFactory
fun interface Factory {
fun create(
timelineMode: Timeline.Mode,
): PollRepository
}
suspend fun getPoll(eventId: EventId): Result<PollContent> = runCatchingExceptions {
getTimelineProvider()
.getOrThrow()
.getActiveTimeline()
.timelineItems
.first()
.asSequence()
.filterIsInstance<MatrixTimelineItem.Event>()
.first { it.eventId == eventId }
.event
.content as PollContent
}
suspend fun savePoll(
existingPollId: EventId?,
question: String,
answers: List<String>,
pollKind: PollKind,
maxSelections: Int,
): Result<Unit> = when (existingPollId) {
null -> getTimelineProvider().flatMap { timelineProvider ->
timelineProvider
.getActiveTimeline()
.createPoll(
question = question,
answers = answers,
maxSelections = maxSelections,
pollKind = pollKind,
)
}
else -> getTimelineProvider().flatMap { timelineProvider ->
timelineProvider.getActiveTimeline()
.editPoll(
pollStartId = existingPollId,
question = question,
answers = answers,
maxSelections = maxSelections,
pollKind = pollKind,
)
}
}
suspend fun deletePoll(
pollStartId: EventId,
): Result<Unit> =
getTimelineProvider().flatMap { timelineProvider ->
timelineProvider.getActiveTimeline()
.redactEvent(
eventOrTransactionId = pollStartId.toEventOrTransactionId(),
reason = null,
)
}
private suspend fun getTimelineProvider(): Result<TimelineProvider> {
return when (timelineMode) {
is Timeline.Mode.Thread -> {
val threadedTimelineResult = room.createTimeline(CreateTimelineParams.Threaded(timelineMode.threadRootId))
threadedTimelineResult.map { threadedTimeline ->
object : TimelineProvider {
private val flow = MutableStateFlow<Timeline?>(threadedTimeline)
override fun activeTimelineFlow(): StateFlow<Timeline?> = flow
}
}
}
else -> Result.success(defaultTimelineProvider)
}
}
}
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.poll.api.history.PollHistoryEntryPoint
import io.element.android.libraries.architecture.createNode
@ContributesBinding(AppScope::class)
class DefaultPollHistoryEntryPoint : PollHistoryEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode<PollHistoryFlowNode>(buildContext)
}
}
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
import io.element.android.libraries.matrix.api.core.EventId
sealed interface PollHistoryEvents {
data object LoadMore : PollHistoryEvents
data class SelectPollAnswer(val pollStartId: EventId, val answerId: String) : PollHistoryEvents
data class EndPoll(val pollStartId: EventId) : PollHistoryEvents
data class SelectFilter(val filter: PollHistoryFilter) : PollHistoryEvents
}
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.poll.api.create.CreatePollEntryPoint
import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.parcelize.Parcelize
@ContributesNode(RoomScope::class)
@AssistedInject
class PollHistoryFlowNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val createPollEntryPoint: CreatePollEntryPoint,
) : BaseFlowNode<PollHistoryFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
) {
sealed interface NavTarget : Parcelable {
@Parcelize
data object Root : NavTarget
@Parcelize
data class EditPoll(val pollStartEventId: EventId) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
is NavTarget.EditPoll -> {
createPollEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = CreatePollEntryPoint.Params(
timelineMode = Timeline.Mode.Live,
mode = CreatePollMode.EditPoll(eventId = navTarget.pollStartEventId)
)
)
}
NavTarget.Root -> {
val callback = object : PollHistoryNode.Callback {
override fun navigateToEditPoll(pollStartEventId: EventId) {
backstack.push(NavTarget.EditPoll(pollStartEventId))
}
}
createNode<PollHistoryNode>(
buildContext = buildContext,
plugins = listOf(callback)
)
}
}
}
@Composable
override fun View(modifier: Modifier) {
BackstackView()
}
}
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
@ContributesNode(RoomScope::class)
@AssistedInject
class PollHistoryNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: PollHistoryPresenter,
) : Node(
buildContext = buildContext,
plugins = plugins,
) {
interface Callback : Plugin {
fun navigateToEditPoll(pollStartEventId: EventId)
}
private val callback: Callback = callback()
@Composable
override fun View(modifier: Modifier) {
PollHistoryView(
state = presenter.present(),
modifier = modifier,
onEditPoll = callback::navigateToEditPoll,
goBack = this::navigateUp,
)
}
}
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
import io.element.android.features.poll.impl.history.model.PollHistoryItems
import io.element.android.features.poll.impl.history.model.PollHistoryItemsFactory
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@Inject
class PollHistoryPresenter(
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
private val sendPollResponseAction: SendPollResponseAction,
private val endPollAction: EndPollAction,
private val pollHistoryItemFactory: PollHistoryItemsFactory,
private val room: JoinedRoom,
) : Presenter<PollHistoryState> {
@Composable
override fun present(): PollHistoryState {
val timeline = room.liveTimeline
val paginationState by timeline.backwardPaginationStatus.collectAsState()
val pollHistoryItemsFlow = remember {
timeline.timelineItems.map { items ->
pollHistoryItemFactory.create(items)
}
}
var activeFilter by rememberSaveable {
mutableStateOf(PollHistoryFilter.ONGOING)
}
val pollHistoryItems by pollHistoryItemsFlow.collectAsState(initial = PollHistoryItems())
LaunchedEffect(paginationState, pollHistoryItems.size) {
if (pollHistoryItems.size == 0 && paginationState.canPaginate) loadMore(timeline)
}
val isLoading by remember {
derivedStateOf {
pollHistoryItems.size == 0 || paginationState.isPaginating
}
}
val coroutineScope = rememberCoroutineScope()
fun handleEvent(event: PollHistoryEvents) {
when (event) {
is PollHistoryEvents.LoadMore -> {
coroutineScope.loadMore(timeline)
}
is PollHistoryEvents.SelectPollAnswer -> sessionCoroutineScope.launch {
sendPollResponseAction.execute(
timeline = timeline,
pollStartId = event.pollStartId,
answerId = event.answerId
)
}
is PollHistoryEvents.EndPoll -> sessionCoroutineScope.launch {
endPollAction.execute(timeline = timeline, pollStartId = event.pollStartId)
}
is PollHistoryEvents.SelectFilter -> {
activeFilter = event.filter
}
}
}
return PollHistoryState(
isLoading = isLoading,
hasMoreToLoad = paginationState.hasMoreToLoad,
pollHistoryItems = pollHistoryItems,
activeFilter = activeFilter,
eventSink = ::handleEvent,
)
}
private fun CoroutineScope.loadMore(pollHistory: Timeline) = launch {
pollHistory.paginate(Timeline.PaginationDirection.BACKWARDS)
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
import io.element.android.features.poll.impl.history.model.PollHistoryItem
import io.element.android.features.poll.impl.history.model.PollHistoryItems
import kotlinx.collections.immutable.ImmutableList
data class PollHistoryState(
val isLoading: Boolean,
val hasMoreToLoad: Boolean,
val activeFilter: PollHistoryFilter,
val pollHistoryItems: PollHistoryItems,
val eventSink: (PollHistoryEvents) -> Unit,
) {
fun pollHistoryForFilter(filter: PollHistoryFilter): ImmutableList<PollHistoryItem> {
return when (filter) {
PollHistoryFilter.ONGOING -> pollHistoryItems.ongoing
PollHistoryFilter.PAST -> pollHistoryItems.past
}
}
}
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.poll.api.pollcontent.PollContentState
import io.element.android.features.poll.api.pollcontent.aPollContentState
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
import io.element.android.features.poll.impl.history.model.PollHistoryItem
import io.element.android.features.poll.impl.history.model.PollHistoryItems
import kotlinx.collections.immutable.toImmutableList
class PollHistoryStateProvider : PreviewParameterProvider<PollHistoryState> {
override val values: Sequence<PollHistoryState>
get() = sequenceOf(
aPollHistoryState(),
aPollHistoryState(
isLoading = true,
hasMoreToLoad = true,
activeFilter = PollHistoryFilter.PAST,
),
aPollHistoryState(
activeFilter = PollHistoryFilter.ONGOING,
currentItems = emptyList(),
),
aPollHistoryState(
activeFilter = PollHistoryFilter.PAST,
currentItems = emptyList(),
),
aPollHistoryState(
activeFilter = PollHistoryFilter.PAST,
currentItems = emptyList(),
hasMoreToLoad = true,
),
)
}
internal fun aPollHistoryState(
isLoading: Boolean = false,
hasMoreToLoad: Boolean = false,
activeFilter: PollHistoryFilter = PollHistoryFilter.ONGOING,
currentItems: List<PollHistoryItem> = listOf(
aPollHistoryItem(),
),
eventSink: (PollHistoryEvents) -> Unit = {},
) = PollHistoryState(
isLoading = isLoading,
hasMoreToLoad = hasMoreToLoad,
activeFilter = activeFilter,
pollHistoryItems = PollHistoryItems(
ongoing = currentItems.toImmutableList(),
past = currentItems.toImmutableList(),
),
eventSink = eventSink,
)
internal fun aPollHistoryItem(
formattedDate: String = "01/12/2023",
state: PollContentState = aPollContentState(),
) = PollHistoryItem(
formattedDate = formattedDate,
state = state,
)
@@ -0,0 +1,265 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.isTraversalGroup
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.poll.api.pollcontent.PollContentView
import io.element.android.features.poll.impl.R
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
import io.element.android.features.poll.impl.history.model.PollHistoryItem
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.SegmentedButton
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PollHistoryView(
state: PollHistoryState,
onEditPoll: (EventId) -> Unit,
goBack: () -> Unit,
modifier: Modifier = Modifier,
) {
fun onLoadMore() {
state.eventSink(PollHistoryEvents.LoadMore)
}
fun onSelectAnswer(pollStartId: EventId, answerId: String) {
state.eventSink(PollHistoryEvents.SelectPollAnswer(pollStartId, answerId))
}
fun onEndPoll(pollStartId: EventId) {
state.eventSink(PollHistoryEvents.EndPoll(pollStartId))
}
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
titleStr = stringResource(R.string.screen_polls_history_title),
navigationIcon = {
BackButton(onClick = goBack)
},
)
},
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
) {
val pagerState = rememberPagerState(state.activeFilter.ordinal, 0f) {
PollHistoryFilter.entries.size
}
LaunchedEffect(state.activeFilter) {
pagerState.scrollToPage(state.activeFilter.ordinal)
}
PollHistoryFilterButtons(
activeFilter = state.activeFilter,
onSelectFilter = { state.eventSink(PollHistoryEvents.SelectFilter(it)) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
)
HorizontalPager(
state = pagerState,
userScrollEnabled = false,
modifier = Modifier.fillMaxSize()
) { page ->
val filter = PollHistoryFilter.entries[page]
val pollHistoryItems = state.pollHistoryForFilter(filter)
PollHistoryList(
filter = filter,
pollHistoryItems = pollHistoryItems,
hasMoreToLoad = state.hasMoreToLoad,
isLoading = state.isLoading,
onSelectAnswer = ::onSelectAnswer,
onEditPoll = onEditPoll,
onEndPoll = ::onEndPoll,
onLoadMore = ::onLoadMore,
modifier = Modifier.fillMaxSize(),
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PollHistoryFilterButtons(
activeFilter: PollHistoryFilter,
onSelectFilter: (PollHistoryFilter) -> Unit,
modifier: Modifier = Modifier,
) {
SingleChoiceSegmentedButtonRow(modifier = modifier) {
PollHistoryFilter.entries.forEach { filter ->
SegmentedButton(
index = filter.ordinal,
count = PollHistoryFilter.entries.size,
selected = activeFilter == filter,
onClick = { onSelectFilter(filter) },
text = stringResource(filter.stringResource),
)
}
}
}
@Composable
private fun PollHistoryList(
filter: PollHistoryFilter,
pollHistoryItems: ImmutableList<PollHistoryItem>,
hasMoreToLoad: Boolean,
isLoading: Boolean,
onSelectAnswer: (pollStartId: EventId, answerId: String) -> Unit,
onEditPoll: (pollStartId: EventId) -> Unit,
onEndPoll: (pollStartId: EventId) -> Unit,
onLoadMore: () -> Unit,
modifier: Modifier = Modifier,
) {
val lazyListState = rememberLazyListState()
LazyColumn(
state = lazyListState,
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
items(pollHistoryItems) { pollHistoryItem ->
PollHistoryItemRow(
pollHistoryItem = pollHistoryItem,
onSelectAnswer = onSelectAnswer,
onEditPoll = onEditPoll,
onEndPoll = onEndPoll,
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
)
}
if (pollHistoryItems.isEmpty()) {
item {
Column(
modifier = Modifier
.fillParentMaxSize()
.padding(bottom = 24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
val emptyStringResource = if (filter == PollHistoryFilter.PAST) {
stringResource(R.string.screen_polls_history_empty_past)
} else {
stringResource(R.string.screen_polls_history_empty_ongoing)
}
Text(
text = emptyStringResource,
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp, horizontal = 16.dp),
textAlign = TextAlign.Center,
)
if (hasMoreToLoad) {
LoadMoreButton(isLoading = isLoading, onClick = onLoadMore)
}
}
}
} else if (hasMoreToLoad) {
item {
LoadMoreButton(isLoading = isLoading, onClick = onLoadMore)
}
}
}
}
@Composable
private fun LoadMoreButton(isLoading: Boolean, onClick: () -> Unit) {
Button(
text = stringResource(CommonStrings.action_load_more),
showProgress = isLoading,
onClick = onClick,
modifier = Modifier.padding(vertical = 24.dp),
)
}
@Composable
private fun PollHistoryItemRow(
pollHistoryItem: PollHistoryItem,
onSelectAnswer: (pollStartId: EventId, answerId: String) -> Unit,
onEditPoll: (pollStartId: EventId) -> Unit,
onEndPoll: (pollStartId: EventId) -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier.semantics(mergeDescendants = true) {
// Allow the answers to be traversed by Talkback
isTraversalGroup = true
},
border = BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary),
shape = RoundedCornerShape(size = 12.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = pollHistoryItem.formattedDate,
color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodySmRegular,
)
Spacer(modifier = Modifier.height(4.dp))
PollContentView(
state = pollHistoryItem.state,
onSelectAnswer = onSelectAnswer,
onEditPoll = onEditPoll,
onEndPoll = onEndPoll,
)
}
}
}
@PreviewsDayNight
@Composable
internal fun PollHistoryViewPreview(
@PreviewParameter(PollHistoryStateProvider::class) state: PollHistoryState
) = ElementPreview {
PollHistoryView(
state = state,
onEditPoll = {},
goBack = {},
)
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history.model
import io.element.android.features.poll.impl.R
enum class PollHistoryFilter(val stringResource: Int) {
ONGOING(R.string.screen_polls_history_filter_ongoing),
PAST(R.string.screen_polls_history_filter_past),
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history.model
import io.element.android.features.poll.api.pollcontent.PollContentState
data class PollHistoryItem(
val formattedDate: String,
val state: PollContentState,
)
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history.model
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
data class PollHistoryItems(
val ongoing: ImmutableList<PollHistoryItem> = persistentListOf(),
val past: ImmutableList<PollHistoryItem> = persistentListOf(),
) {
val size = ongoing.size + past.size
}
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.history.model
import dev.zacsweers.metro.Inject
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.withContext
@Inject
class PollHistoryItemsFactory(
private val pollContentStateFactory: PollContentStateFactory,
private val dateFormatter: DateFormatter,
private val dispatchers: CoroutineDispatchers,
) {
suspend fun create(timelineItems: List<MatrixTimelineItem>): PollHistoryItems = withContext(dispatchers.computation) {
val past = ArrayList<PollHistoryItem>()
val ongoing = ArrayList<PollHistoryItem>()
for (index in timelineItems.indices.reversed()) {
val timelineItem = timelineItems[index]
val pollHistoryItem = create(timelineItem) ?: continue
if (pollHistoryItem.state.isPollEnded) {
past.add(pollHistoryItem)
} else {
ongoing.add(pollHistoryItem)
}
}
PollHistoryItems(
ongoing = ongoing.toImmutableList(),
past = past.toImmutableList()
)
}
private suspend fun create(timelineItem: MatrixTimelineItem): PollHistoryItem? {
return when (timelineItem) {
is MatrixTimelineItem.Event -> {
val pollContent = timelineItem.event.content as? PollContent ?: return null
val pollContentState = pollContentStateFactory.create(
eventId = timelineItem.eventId,
isEditable = timelineItem.event.isEditable,
isOwn = timelineItem.event.isOwn,
content = pollContent,
)
PollHistoryItem(
formattedDate = dateFormatter.format(
timestamp = timelineItem.event.timestamp,
mode = DateFormatterMode.Day,
useRelative = true
),
state = pollContentState
)
}
else -> null
}
}
}
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.poll.impl.model
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.poll.api.pollcontent.PollAnswerItem
import io.element.android.features.poll.api.pollcontent.PollContentState
import io.element.android.features.poll.api.pollcontent.PollContentStateFactory
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.isDisclosed
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import kotlinx.collections.immutable.toImmutableList
@ContributesBinding(RoomScope::class)
class DefaultPollContentStateFactory(
private val matrixClient: MatrixClient,
) : PollContentStateFactory {
override suspend fun create(
eventId: EventId?,
isEditable: Boolean,
isOwn: Boolean,
content: PollContent,
): PollContentState {
val totalVoteCount = content.votes.flatMap { it.value }.size
val myVotes = content.votes.filter { matrixClient.sessionId in it.value }.keys
val isPollEnded = content.endTime != null
val winnerIds = if (!isPollEnded) {
emptyList()
} else {
content.answers
.map { answer -> answer.id }
.groupBy { answerId -> content.votes[answerId]?.size ?: 0 } // Group by votes count
.maxByOrNull { (votes, _) -> votes } // Keep max voted answers
?.takeIf { (votes, _) -> votes > 0 } // Ignore if no option has been voted
?.value
.orEmpty()
}
val answerItems = content.answers.map { answer ->
val answerVoteCount = content.votes[answer.id]?.size ?: 0
val isSelected = answer.id in myVotes
val isWinner = answer.id in winnerIds
val percentage = if (totalVoteCount > 0) answerVoteCount.toFloat() / totalVoteCount.toFloat() else 0f
PollAnswerItem(
answer = answer,
isSelected = isSelected,
isEnabled = !isPollEnded,
isWinner = isWinner,
showVotes = content.kind.isDisclosed || isPollEnded,
votesCount = answerVoteCount,
percentage = percentage,
)
}
return PollContentState(
eventId = eventId,
question = content.question,
answerItems = answerItems.toImmutableList(),
pollKind = content.kind,
isPollEditable = isEditable,
isPollEnded = isPollEnded,
isMine = isOwn,
)
}
}
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Дадаць варыянт"</string>
<string name="screen_create_poll_anonymous_desc">"Паказаць вынікі толькі пасля заканчэння апытання"</string>
<string name="screen_create_poll_anonymous_headline">"Схаваць галасы"</string>
<string name="screen_create_poll_answer_hint">"Варыянт %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Вашы змены не былі захаваны. Вы ўпэўнены, што хочаце вярнуцца?"</string>
<string name="screen_create_poll_question_desc">"Пытанне або тэма"</string>
<string name="screen_create_poll_question_hint">"Пра што апытанне?"</string>
<string name="screen_create_poll_title">"Стварэнне апытання"</string>
<string name="screen_edit_poll_delete_confirmation">"Вы ўпэўнены, што хочаце выдаліць гэтае апытанне?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Выдаліць апытанне"</string>
<string name="screen_edit_poll_title">"Рэдагаваць апытанне"</string>
<string name="screen_polls_history_empty_ongoing">"Немагчыма знайсці бягучыя апытанні."</string>
<string name="screen_polls_history_empty_past">"Немагчыма знайсці мінулыя апытанні."</string>
<string name="screen_polls_history_filter_ongoing">"Бягучыя"</string>
<string name="screen_polls_history_filter_past">"Мінулыя"</string>
<string name="screen_polls_history_title">"Апытанні"</string>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Добавяне на опция"</string>
<string name="screen_create_poll_anonymous_desc">"Показване на резултатите само след приключване на анкетата"</string>
<string name="screen_create_poll_anonymous_headline">"Скриване на гласовете"</string>
<string name="screen_create_poll_answer_hint">"Опция %1$d"</string>
<string name="screen_create_poll_question_desc">"Въпрос или тема"</string>
<string name="screen_create_poll_question_hint">"За какво се отнася анкетата?"</string>
<string name="screen_create_poll_title">"Създаване на анкета"</string>
<string name="screen_edit_poll_delete_confirmation">"Сигурни ли сте, че искате да изтриете тази анкета?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Изтриване на анкетата"</string>
<string name="screen_edit_poll_title">"Редактиране на анкетата"</string>
<string name="screen_polls_history_empty_ongoing">"Не се намират текущи анкети."</string>
<string name="screen_polls_history_empty_past">"Не се намират приключили анкети."</string>
<string name="screen_polls_history_filter_ongoing">"Текущи"</string>
<string name="screen_polls_history_filter_past">"Приключили"</string>
<string name="screen_polls_history_title">"Анкети"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Přidat volbu"</string>
<string name="screen_create_poll_anonymous_desc">"Zobrazit výsledky až po skončení hlasování"</string>
<string name="screen_create_poll_anonymous_headline">"Anonymní hlasování"</string>
<string name="screen_create_poll_answer_hint">"Volba %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Vaše změny nebyly uloženy. Opravdu se chcete vrátit?"</string>
<string name="screen_create_poll_delete_option_a11y">"Smazat možnost %1$s"</string>
<string name="screen_create_poll_question_desc">"Otázka nebo téma"</string>
<string name="screen_create_poll_question_hint">"Čeho se hlasování týká?"</string>
<string name="screen_create_poll_title">"Vytvořit hlasování"</string>
<string name="screen_edit_poll_delete_confirmation">"Opravdu chcete odstranit toto hlasování?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Odstranit hlasování"</string>
<string name="screen_edit_poll_title">"Upravit hlasování"</string>
<string name="screen_polls_history_empty_ongoing">"Nelze najít žádná probíhající hlasování."</string>
<string name="screen_polls_history_empty_past">"Nelze najít žádná minulá hlasování."</string>
<string name="screen_polls_history_filter_ongoing">"Probíhající"</string>
<string name="screen_polls_history_filter_past">"Minulé"</string>
<string name="screen_polls_history_title">"Hlasování"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Ychwanegu dewis"</string>
<string name="screen_create_poll_anonymous_desc">"Dangos canlyniadau dim ond ar ôl i\'r pleidleisio ddod i ben"</string>
<string name="screen_create_poll_anonymous_headline">"Cuddio pleidleisiau"</string>
<string name="screen_create_poll_answer_hint">"Dewis %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Dyw eich newidiadau heb gael eu cadw. Ydych chi\'n siŵr eich bod am fynd nôl?"</string>
<string name="screen_create_poll_delete_option_a11y">"Dileu opsiwn %1$s"</string>
<string name="screen_create_poll_question_desc">"Cwestiwn neu bwnc"</string>
<string name="screen_create_poll_question_hint">"Am beth mae\'r bleidlais?"</string>
<string name="screen_create_poll_title">"Creu Pleidlais"</string>
<string name="screen_edit_poll_delete_confirmation">"Ydych chi\'n siŵr eich bod am ddileu\'r bleidlais hon?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Dileu Pleidlais"</string>
<string name="screen_edit_poll_title">"Golygu pleidlais"</string>
<string name="screen_polls_history_empty_ongoing">"Methu dod o hyd i unrhyw bleidleisiau cyfredol."</string>
<string name="screen_polls_history_empty_past">"Methu dod o hyd i unrhyw bleidleisiau blaenorol."</string>
<string name="screen_polls_history_filter_ongoing">"Parhaus"</string>
<string name="screen_polls_history_filter_past">"Gorffennol"</string>
<string name="screen_polls_history_title">"Pleidleisiau"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Tilføj mulighed"</string>
<string name="screen_create_poll_anonymous_desc">"Vis først resultater, når afstemningen er slut"</string>
<string name="screen_create_poll_anonymous_headline">"Skjul stemmer"</string>
<string name="screen_create_poll_answer_hint">"Mulighed %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Dine ændringer er ikke blevet gemt. Er du sikker på, at du vil gå tilbage?"</string>
<string name="screen_create_poll_delete_option_a11y">"Slet mulighed %1$s"</string>
<string name="screen_create_poll_question_desc">"Spørgsmål eller emne"</string>
<string name="screen_create_poll_question_hint">"Hvad handler afstemningen om?"</string>
<string name="screen_create_poll_title">"Opret afstemning"</string>
<string name="screen_edit_poll_delete_confirmation">"Er du sikker på, at du ønsker at slette denne afstemning?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Slet afstemning"</string>
<string name="screen_edit_poll_title">"Redigér afstemning"</string>
<string name="screen_polls_history_empty_ongoing">"Kan ikke finde nogen igangværende afstemninger."</string>
<string name="screen_polls_history_empty_past">"Kan ikke finde nogen tidligere afstemninger."</string>
<string name="screen_polls_history_filter_ongoing">"Igangværende"</string>
<string name="screen_polls_history_filter_past">"Tidligere"</string>
<string name="screen_polls_history_title">"Afstemninger"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Option hinzufügen"</string>
<string name="screen_create_poll_anonymous_desc">"Ergebnisse erst nach Ende der Umfrage anzeigen"</string>
<string name="screen_create_poll_anonymous_headline">"Anonyme Umfrage"</string>
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Deine Änderungen wurden nicht gespeichert. Bist du sicher, dass du zurückgehen willst?"</string>
<string name="screen_create_poll_delete_option_a11y">"Lösche Option %1$s"</string>
<string name="screen_create_poll_question_desc">"Frage oder Thema"</string>
<string name="screen_create_poll_question_hint">"Worum geht es bei der Umfrage?"</string>
<string name="screen_create_poll_title">"Umfrage erstellen"</string>
<string name="screen_edit_poll_delete_confirmation">"Möchtest du diese Umfrage wirklich löschen?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Umfrage löschen"</string>
<string name="screen_edit_poll_title">"Umfrage bearbeiten"</string>
<string name="screen_polls_history_empty_ongoing">"Keine laufenden Umfragen vorhanden."</string>
<string name="screen_polls_history_empty_past">"Keine beendeten Umfragen vorhanden."</string>
<string name="screen_polls_history_filter_ongoing">"Laufend"</string>
<string name="screen_polls_history_filter_past">"Beendet"</string>
<string name="screen_polls_history_title">"Umfragen"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Προσθήκη επιλογής"</string>
<string name="screen_create_poll_anonymous_desc">"Εμφάνιση αποτελεσμάτων μόνο μετά τη λήξη της ψηφοφορίας"</string>
<string name="screen_create_poll_anonymous_headline">"Απόκρυψη ψήφων"</string>
<string name="screen_create_poll_answer_hint">"Επιλογή %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Οι αλλαγές σου δεν έχουν αποθηκευτεί. Σίγουρα θες να πας πίσω;"</string>
<string name="screen_create_poll_delete_option_a11y">"Διαγραφή επιλογής %1$s"</string>
<string name="screen_create_poll_question_desc">"Ερώτηση ή θέμα"</string>
<string name="screen_create_poll_question_hint">"Τί αφορά η δημοσκόπηση;"</string>
<string name="screen_create_poll_title">"Δημιουργία Δημοσκόπησης"</string>
<string name="screen_edit_poll_delete_confirmation">"Θες σίγουρα να διαγράψεις αυτήν τη δημοσκόπηση;"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Διαγραφή Δημοσκόπησης"</string>
<string name="screen_edit_poll_title">"Επεξεργασία δημοσκόπησης"</string>
<string name="screen_polls_history_empty_ongoing">"Δεν είναι δυνατή η εύρεση ενεργών δημοσκοπήσεων"</string>
<string name="screen_polls_history_empty_past">"Δεν είναι δυνατή η εύρεση παλιών δημοσκοπήσεων"</string>
<string name="screen_polls_history_filter_ongoing">"Σε εξέλιξη"</string>
<string name="screen_polls_history_filter_past">"Παρελθόν"</string>
<string name="screen_polls_history_title">"Δημοσκοπήσεις"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Añadir opción"</string>
<string name="screen_create_poll_anonymous_desc">"Mostrar los resultados solo después de que finalice la encuesta"</string>
<string name="screen_create_poll_anonymous_headline">"Ocultar votos"</string>
<string name="screen_create_poll_answer_hint">"Opción %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Tus cambios no se han guardado. ¿Estás seguro de que quieres volver atrás?"</string>
<string name="screen_create_poll_question_desc">"Pregunta o tema"</string>
<string name="screen_create_poll_question_hint">"¿De qué trata la encuesta?"</string>
<string name="screen_create_poll_title">"Crear una Encuesta"</string>
<string name="screen_edit_poll_delete_confirmation">"¿Seguro que quieres eliminar esta encuesta?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Eliminar encuesta"</string>
<string name="screen_edit_poll_title">"Editar encuesta"</string>
<string name="screen_polls_history_empty_ongoing">"No se pudo encontrar ninguna encuesta en curso."</string>
<string name="screen_polls_history_empty_past">"No se pudo encontrar ninguna encuesta anterior."</string>
<string name="screen_polls_history_filter_ongoing">"En curso"</string>
<string name="screen_polls_history_filter_past">"Anteriores"</string>
<string name="screen_polls_history_title">"Encuestas"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Lisa veel üks valik"</string>
<string name="screen_create_poll_anonymous_desc">"Näita tulemusi alles pärast küsitluse lõppu"</string>
<string name="screen_create_poll_anonymous_headline">"Peida hääled"</string>
<string name="screen_create_poll_answer_hint">"Valik %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Sinu tehtud muudatused pole veel salvestatud. Kas sa oled kindel, et soovid minna tagasi?"</string>
<string name="screen_create_poll_delete_option_a11y">"Kustuta valik: %1$s"</string>
<string name="screen_create_poll_question_desc">"Küsimus või teema"</string>
<string name="screen_create_poll_question_hint">"Mis on küsitluse teema?"</string>
<string name="screen_create_poll_title">"Loo küsitlus"</string>
<string name="screen_edit_poll_delete_confirmation">"Kas sa oled kindel, et soovid selle küsitluse kustutada?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Kustuta küsitlus"</string>
<string name="screen_edit_poll_title">"Muuda küsitlust"</string>
<string name="screen_polls_history_empty_ongoing">"Ei leia ühtegi käimasolevat küsitlust."</string>
<string name="screen_polls_history_empty_past">"Ei leia ühtegi varasemat küsitlust."</string>
<string name="screen_polls_history_filter_ongoing">"Käimasolev küsitlus"</string>
<string name="screen_polls_history_filter_past">"Varasem küsitlus"</string>
<string name="screen_polls_history_title">"Küsitlused"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Gehitu aukera"</string>
<string name="screen_create_poll_anonymous_desc">"Erakutsi emaitzak inkesta amaitutakoan soilik"</string>
<string name="screen_create_poll_anonymous_headline">"Ezkutatu botoak"</string>
<string name="screen_create_poll_answer_hint">"%1$d. aukera"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Zure aldaketak ez dira gorde. Ziur itzuli nahi duzula?"</string>
<string name="screen_create_poll_question_desc">"Galdera edo gaia"</string>
<string name="screen_create_poll_question_hint">"Zeri buruzko inkesta da?"</string>
<string name="screen_create_poll_title">"Sortu inkesta"</string>
<string name="screen_edit_poll_delete_confirmation">"Ziur inkesta hau ezabatu nahi duzula?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Ezabatu inkesta"</string>
<string name="screen_edit_poll_title">"Editatu inkesta"</string>
<string name="screen_polls_history_empty_ongoing">"Ezin da abian dagoen inkestarik aurkitu."</string>
<string name="screen_polls_history_empty_past">"Ezin da iraungitako inkestarik aurkitu."</string>
<string name="screen_polls_history_filter_ongoing">"Abian direnak"</string>
<string name="screen_polls_history_filter_past">"Iraungitakoak"</string>
<string name="screen_polls_history_title">"Inkestak"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"افزودن گزینه"</string>
<string name="screen_create_poll_anonymous_desc">"نمایش نتیجه‌ها تنها پس از پایان نظرسنجی"</string>
<string name="screen_create_poll_anonymous_headline">"نهفتن رأی‌ها"</string>
<string name="screen_create_poll_answer_hint">"گزینهٔ %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"تغییراتتان ذخیره نشده‌اند. مطمئنید که می‌خواهید برگردید؟"</string>
<string name="screen_create_poll_question_desc">"پرسش یا موضوع"</string>
<string name="screen_create_poll_question_hint">"این نظرسنجی دربارهٔ چیست؟"</string>
<string name="screen_create_poll_title">"ایجاد نظرسنجی"</string>
<string name="screen_edit_poll_delete_confirmation">"مطمئنید که می‌خواهید این نظرسنجی را حذف کنید؟"</string>
<string name="screen_edit_poll_delete_confirmation_title">"حذف نظرسنجی"</string>
<string name="screen_edit_poll_title">"ویرایش نظرسنجی"</string>
<string name="screen_polls_history_empty_ongoing">"نتوانست هیچ نظرسنجی در جریانی بیابد."</string>
<string name="screen_polls_history_empty_past">"نتوانست هیچ نظرسنجی گذشته‌ای بیابد."</string>
<string name="screen_polls_history_filter_ongoing">"در جریان"</string>
<string name="screen_polls_history_filter_past">"گذشته"</string>
<string name="screen_polls_history_title">"نظرسنجی‌ها"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Lisää vaihtoehto"</string>
<string name="screen_create_poll_anonymous_desc">"Näytä tulokset vasta kyselyn päätyttyä"</string>
<string name="screen_create_poll_anonymous_headline">"Piilota äänet"</string>
<string name="screen_create_poll_answer_hint">"Vaihtoehto %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Muutoksiasi ei ole tallennettu. Haluatko varmasti palata takaisin?"</string>
<string name="screen_create_poll_delete_option_a11y">"Poista vaihtoehto %1$s"</string>
<string name="screen_create_poll_question_desc">"Kysymys tai aihe"</string>
<string name="screen_create_poll_question_hint">"Mistä kyselyssä on kyse?"</string>
<string name="screen_create_poll_title">"Luo kysely"</string>
<string name="screen_edit_poll_delete_confirmation">"Haluatko varmasti poistaa tämän kyselyn?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Poista kysely"</string>
<string name="screen_edit_poll_title">"Muokkaa kyselyä"</string>
<string name="screen_polls_history_empty_ongoing">"Meneillään olevia kyselyjä ei löytynyt."</string>
<string name="screen_polls_history_empty_past">"Aiempia kyselyjä ei löytynyt."</string>
<string name="screen_polls_history_filter_ongoing">"Meneillään olevat"</string>
<string name="screen_polls_history_filter_past">"Aiemmat"</string>
<string name="screen_polls_history_title">"Kyselyt"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Ajouter une option"</string>
<string name="screen_create_poll_anonymous_desc">"Afficher les résultats uniquement après la fin du sondage"</string>
<string name="screen_create_poll_anonymous_headline">"Masquer les votes"</string>
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Vos modifications nont pas été enregistrées. Êtes-vous certain de vouloir quitter ?"</string>
<string name="screen_create_poll_delete_option_a11y">"Supprimer loption %1$s"</string>
<string name="screen_create_poll_question_desc">"Question ou sujet"</string>
<string name="screen_create_poll_question_hint">"Quel est le sujet du sondage ?"</string>
<string name="screen_create_poll_title">"Créer un sondage"</string>
<string name="screen_edit_poll_delete_confirmation">"Êtes-vous certain de vouloir supprimer ce sondage ?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Supprimer le sondage"</string>
<string name="screen_edit_poll_title">"Modifier le sondage"</string>
<string name="screen_polls_history_empty_ongoing">"Impossible de trouver des sondages en cours."</string>
<string name="screen_polls_history_empty_past">"Impossible de trouver des sondages terminés."</string>
<string name="screen_polls_history_filter_ongoing">"En cours"</string>
<string name="screen_polls_history_filter_past">"Terminés"</string>
<string name="screen_polls_history_title">"Sondages"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Lehetőség hozzáadása"</string>
<string name="screen_create_poll_anonymous_desc">"Eredmények megjelenítése csak a szavazás befejezése után"</string>
<string name="screen_create_poll_anonymous_headline">"Szavazatok elrejtése"</string>
<string name="screen_create_poll_answer_hint">"%1$d. lehetőség"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"A módosítások nem lettek mentve. Biztos, hogy visszalép?"</string>
<string name="screen_create_poll_delete_option_a11y">"Lehetőség törlése: %1$s"</string>
<string name="screen_create_poll_question_desc">"Kérdés vagy téma"</string>
<string name="screen_create_poll_question_hint">"Miről szól ez a szavazás?"</string>
<string name="screen_create_poll_title">"Szavazás létrehozása"</string>
<string name="screen_edit_poll_delete_confirmation">"Biztos, hogy törli ezt a szavazást?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Szavazás törlése"</string>
<string name="screen_edit_poll_title">"Szavazás szerkesztése"</string>
<string name="screen_polls_history_empty_ongoing">"Nem találhatók folyamatban lévő szavazások."</string>
<string name="screen_polls_history_empty_past">"Nem található korábbi szavazás."</string>
<string name="screen_polls_history_filter_ongoing">"Folyamatban"</string>
<string name="screen_polls_history_filter_past">"Korábbi"</string>
<string name="screen_polls_history_title">"Szavazások"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Tambahkan opsi"</string>
<string name="screen_create_poll_anonymous_desc">"Tampilkan hasil hanya setelah pemungutan suara berakhir"</string>
<string name="screen_create_poll_anonymous_headline">"Pemungutan suara anonim"</string>
<string name="screen_create_poll_answer_hint">"Opsi %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Perubahan Anda belum disimpan. Apakah Anda yakin ingin kembali?"</string>
<string name="screen_create_poll_delete_option_a11y">"Hapus opsi %1$s"</string>
<string name="screen_create_poll_question_desc">"Pertanyaan atau topik"</string>
<string name="screen_create_poll_question_hint">"Tentang apa pemungutan suara ini?"</string>
<string name="screen_create_poll_title">"Buat pemungutan suara"</string>
<string name="screen_edit_poll_delete_confirmation">"Apakah Anda yakin ingin menghapus pemungutan suara ini?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Hapus pemungutan suara"</string>
<string name="screen_edit_poll_title">"Sunting pemungutan suara"</string>
<string name="screen_polls_history_empty_ongoing">"Tidak dapat menemukan pemungutan suara yang sedang berlangsung."</string>
<string name="screen_polls_history_empty_past">"Tidak dapat menemukan pemungutan suara sebelumnya."</string>
<string name="screen_polls_history_filter_ongoing">"Sedang berlangsung"</string>
<string name="screen_polls_history_filter_past">"Masa lalu"</string>
<string name="screen_polls_history_title">"Pemungutan suara"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Aggiungi opzione"</string>
<string name="screen_create_poll_anonymous_desc">"Mostra i risultati solo al termine del sondaggio"</string>
<string name="screen_create_poll_anonymous_headline">"Nascondi voti"</string>
<string name="screen_create_poll_answer_hint">"Opzione %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Le modifiche non sono state salvate. Vuoi davvero tornare indietro?"</string>
<string name="screen_create_poll_delete_option_a11y">"Elimina l\'opzione %1$s"</string>
<string name="screen_create_poll_question_desc">"Domanda o argomento"</string>
<string name="screen_create_poll_question_hint">"Di cosa parla il sondaggio?"</string>
<string name="screen_create_poll_title">"Crea sondaggio"</string>
<string name="screen_edit_poll_delete_confirmation">"Vuoi davvero eliminare questo sondaggio?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Elimina sondaggio"</string>
<string name="screen_edit_poll_title">"Modifica sondaggio"</string>
<string name="screen_polls_history_empty_ongoing">"Impossibile trovare sondaggi in corso."</string>
<string name="screen_polls_history_empty_past">"Impossibile trovare sondaggi passati."</string>
<string name="screen_polls_history_filter_ongoing">"In corso"</string>
<string name="screen_polls_history_filter_past">"Passato"</string>
<string name="screen_polls_history_title">"Sondaggi"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"ვარიანტის დამატება"</string>
<string name="screen_create_poll_anonymous_desc">"შედეგების ჩვენება მხოლოდ გამოკითხვის დასრულების შემდეგ"</string>
<string name="screen_create_poll_anonymous_headline">"ხმების დამალვა"</string>
<string name="screen_create_poll_answer_hint">"ვარიანტი %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"თქვენი ცვლილებები არაა შენახული. დარწმუნებული ხართ დაბრუნებაში?"</string>
<string name="screen_create_poll_question_desc">"კითხვა ან თემა"</string>
<string name="screen_create_poll_question_hint">"რას ეხება გამოკითხვა?"</string>
<string name="screen_create_poll_title">"გამოკითხვის შექმნა"</string>
<string name="screen_edit_poll_delete_confirmation">"დარწმუნებული ხართ, რომ გსურთ ამ გამოკითხვის წაშლა?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"გამოკითხვის წაშლა"</string>
<string name="screen_edit_poll_title">"გამოკითხვის რედაქტირება"</string>
<string name="screen_polls_history_empty_ongoing">"მიმდინარე გამოკითხვები ვერ მოიძებნა."</string>
<string name="screen_polls_history_empty_past">"ბოლო გამოკითხვების მოძებნა ვერ მოხერხდა."</string>
<string name="screen_polls_history_filter_ongoing">"მიმდინარე"</string>
<string name="screen_polls_history_filter_past">"წარსული"</string>
<string name="screen_polls_history_title">"გამოკითხვები"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"옵션 추가"</string>
<string name="screen_create_poll_anonymous_desc">"투표가 끝난 이후에만 결과 표시"</string>
<string name="screen_create_poll_anonymous_headline">"투표 숨기기"</string>
<string name="screen_create_poll_answer_hint">"옵션 %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"변경 내용이 저장되지 않았습니다. 정말로 돌아가시겠습니까?"</string>
<string name="screen_create_poll_delete_option_a11y">"삭제 옵션 %1$s"</string>
<string name="screen_create_poll_question_desc">"질문 또는 주제"</string>
<string name="screen_create_poll_question_hint">"무슨 투표인가요?"</string>
<string name="screen_create_poll_title">"투표 생성"</string>
<string name="screen_edit_poll_delete_confirmation">"정말 이 투표를 삭제하시겠습니까?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"투표 삭제"</string>
<string name="screen_edit_poll_title">"투표 수정"</string>
<string name="screen_polls_history_empty_ongoing">"진행 중인 투표를 찾을 수 없습니다."</string>
<string name="screen_polls_history_empty_past">"과거의 투표를 찾을 수 없습니다."</string>
<string name="screen_polls_history_filter_ongoing">"진행 중"</string>
<string name="screen_polls_history_filter_past">"과거"</string>
<string name="screen_polls_history_title">"투표"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Legg til alternativ"</string>
<string name="screen_create_poll_anonymous_desc">"Vis resultater bare etter at avstemningen er avsluttet"</string>
<string name="screen_create_poll_anonymous_headline">"Skjul stemmer"</string>
<string name="screen_create_poll_answer_hint">"Alternativ %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Endringene dine er ikke lagret. Er du sikker på at du vil gå tilbake?"</string>
<string name="screen_create_poll_delete_option_a11y">"Slett alternativet %1$s"</string>
<string name="screen_create_poll_question_desc">"Spørsmål eller emne"</string>
<string name="screen_create_poll_question_hint">"Hva handler avstemningen om?"</string>
<string name="screen_create_poll_title">"Opprett avstemning"</string>
<string name="screen_edit_poll_delete_confirmation">"Er du sikker på at du vil slette denne avstemningen?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Slett avstemning"</string>
<string name="screen_edit_poll_title">"Rediger avstemning"</string>
<string name="screen_polls_history_empty_ongoing">"Finner ingen pågående avstemninger."</string>
<string name="screen_polls_history_empty_past">"Kan ikke finne noen tidligere avstemninger."</string>
<string name="screen_polls_history_filter_ongoing">"Pågående"</string>
<string name="screen_polls_history_filter_past">"Fortid"</string>
<string name="screen_polls_history_title">"Avstemninger"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Optie toevoegen"</string>
<string name="screen_create_poll_anonymous_desc">"Resultaten pas weergeven nadat de peiling is afgelopen"</string>
<string name="screen_create_poll_anonymous_headline">"Stemmen verbergen"</string>
<string name="screen_create_poll_answer_hint">"Optie %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Je wijzigingen zijn niet opgeslagen. Weet je zeker dat je terug wilt gaan?"</string>
<string name="screen_create_poll_question_desc">"Vraag of onderwerp"</string>
<string name="screen_create_poll_question_hint">"Waar gaat de peiling over?"</string>
<string name="screen_create_poll_title">"Peiling maken"</string>
<string name="screen_edit_poll_delete_confirmation">"Weet je zeker dat je deze peiling wilt verwijderen?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Peiling verwijderen"</string>
<string name="screen_edit_poll_title">"Peiling wijzigen"</string>
<string name="screen_polls_history_empty_ongoing">"Kan geen actieve peilingen vinden."</string>
<string name="screen_polls_history_empty_past">"Kan geen eerdere peilingen vinden."</string>
<string name="screen_polls_history_filter_ongoing">"Actief"</string>
<string name="screen_polls_history_filter_past">"Afgelopen"</string>
<string name="screen_polls_history_title">"Peilingen"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Dodaj opcję"</string>
<string name="screen_create_poll_anonymous_desc">"Pokaż wyniki dopiero po zakończeniu ankiety"</string>
<string name="screen_create_poll_anonymous_headline">"Ukryj głosy"</string>
<string name="screen_create_poll_answer_hint">"Opcja %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Zmiany nie zostały zapisane. Czy na pewno chcesz wrócić?"</string>
<string name="screen_create_poll_delete_option_a11y">"Usuń opcję %1$s"</string>
<string name="screen_create_poll_question_desc">"Pytanie lub temat"</string>
<string name="screen_create_poll_question_hint">"Czego dotyczy ankieta?"</string>
<string name="screen_create_poll_title">"Utwórz ankietę"</string>
<string name="screen_edit_poll_delete_confirmation">"Czy na pewno chcesz usunąć tę ankietę?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Usuń ankietę"</string>
<string name="screen_edit_poll_title">"Edytuj ankietę"</string>
<string name="screen_polls_history_empty_ongoing">"Nie znaleziono ankiet w trakcie."</string>
<string name="screen_polls_history_empty_past">"Nie znaleziono ankiet."</string>
<string name="screen_polls_history_filter_ongoing">"W trakcie"</string>
<string name="screen_polls_history_filter_past">"Przeszłe"</string>
<string name="screen_polls_history_title">"Ankiety"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Adicionar opção"</string>
<string name="screen_create_poll_anonymous_desc">"Mostrar resultados somente após o término da enquete"</string>
<string name="screen_create_poll_anonymous_headline">"Ocultar votos"</string>
<string name="screen_create_poll_answer_hint">"%1$dª opção"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Suas alterações não foram salvas. Tem certeza de que você quer voltar?"</string>
<string name="screen_create_poll_delete_option_a11y">"Apagar opção %1$s"</string>
<string name="screen_create_poll_question_desc">"Pergunta ou tópico"</string>
<string name="screen_create_poll_question_hint">"Sobre o que é a enquete?"</string>
<string name="screen_create_poll_title">"Criar enquete"</string>
<string name="screen_edit_poll_delete_confirmation">"Tem certeza de que quer apagar esta enquete?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Excluir enquete"</string>
<string name="screen_edit_poll_title">"Editar enquete"</string>
<string name="screen_polls_history_empty_ongoing">"Não foi possível encontrar nenhuma enquete em andamento."</string>
<string name="screen_polls_history_empty_past">"Não foi possível encontrar nenhuma enquete anterior."</string>
<string name="screen_polls_history_filter_ongoing">"Em andamento"</string>
<string name="screen_polls_history_filter_past">"Anteriores"</string>
<string name="screen_polls_history_title">"Enquetes"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Adicionar opção"</string>
<string name="screen_create_poll_anonymous_desc">"Mostrar resultados só após o da sondagem"</string>
<string name="screen_create_poll_anonymous_headline">"Ocultar votos"</string>
<string name="screen_create_poll_answer_hint">"Opção %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"As tuas alterações não foram guardadas. Tens a certeza que queres voltar atrás?"</string>
<string name="screen_create_poll_delete_option_a11y">"Eliminar opção %1$s"</string>
<string name="screen_create_poll_question_desc">"Pergunta ou tópico"</string>
<string name="screen_create_poll_question_hint">"De que trata a sondagem?"</string>
<string name="screen_create_poll_title">"Criar sondagem"</string>
<string name="screen_edit_poll_delete_confirmation">"Tens a certeza que queres apagar esta sondagem?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Eliminar sondagem"</string>
<string name="screen_edit_poll_title">"Editar sondagem"</string>
<string name="screen_polls_history_empty_ongoing">"Não foi possível encontrar nenhuma sondagem em curso."</string>
<string name="screen_polls_history_empty_past">"Não foi possível encontrar nenhuma sondagem anterior."</string>
<string name="screen_polls_history_filter_ongoing">"Em curso"</string>
<string name="screen_polls_history_filter_past">"Passado"</string>
<string name="screen_polls_history_title">"Sondagens"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Adăugați o opțiune"</string>
<string name="screen_create_poll_anonymous_desc">"Afișați rezultatele numai după încheierea sondajului"</string>
<string name="screen_create_poll_anonymous_headline">"Sondaj anonim"</string>
<string name="screen_create_poll_answer_hint">"Opțiune %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Modificările dumneavoastră nu au fost salvate. Sunteți sigur că doriți să vă întoarceți?"</string>
<string name="screen_create_poll_delete_option_a11y">"Ștergeți opțiunea %1$s"</string>
<string name="screen_create_poll_question_desc">"Întrebare sau subiect"</string>
<string name="screen_create_poll_question_hint">"Despre ce este sondajul?"</string>
<string name="screen_create_poll_title">"Creați un sondaj"</string>
<string name="screen_edit_poll_delete_confirmation">"Sunteți sigur că doriți să ștergeți acest sondaj?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Ștergeți sondajul"</string>
<string name="screen_edit_poll_title">"Editați sondajul"</string>
<string name="screen_polls_history_empty_ongoing">"Nu s-au putut găsi sondaje în curs de desfășurare."</string>
<string name="screen_polls_history_empty_past">"Nu s-au putut găsi sondaje anterioare."</string>
<string name="screen_polls_history_filter_ongoing">"În desfășurare"</string>
<string name="screen_polls_history_filter_past">"Trecut"</string>
<string name="screen_polls_history_title">"Sondaje"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Добавить вариант"</string>
<string name="screen_create_poll_anonymous_desc">"Показывать результаты только после окончания опроса"</string>
<string name="screen_create_poll_anonymous_headline">"Скрыть голоса"</string>
<string name="screen_create_poll_answer_hint">"Вариант %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Изменения не сохранены. Вы действительно хотите вернуться?"</string>
<string name="screen_create_poll_delete_option_a11y">"Удалить опцию %1$s"</string>
<string name="screen_create_poll_question_desc">"Вопрос или тема"</string>
<string name="screen_create_poll_question_hint">"О чём будет опрос?"</string>
<string name="screen_create_poll_title">"Создать опрос"</string>
<string name="screen_edit_poll_delete_confirmation">"Вы уверены, что хотите удалить этот опрос?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Удалить опрос"</string>
<string name="screen_edit_poll_title">"Редактировать опрос"</string>
<string name="screen_polls_history_empty_ongoing">"Не найдено текущих опросов."</string>
<string name="screen_polls_history_empty_past">"Не найдено прошлых опросов."</string>
<string name="screen_polls_history_filter_ongoing">"Текущие"</string>
<string name="screen_polls_history_filter_past">"Прошлые"</string>
<string name="screen_polls_history_title">"Опросы"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Pridať možnosť"</string>
<string name="screen_create_poll_anonymous_desc">"Zobraziť výsledky až po skončení ankety"</string>
<string name="screen_create_poll_anonymous_headline">"Anonymná anketa"</string>
<string name="screen_create_poll_answer_hint">"Možnosť %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Vaše zmeny neboli uložené. Naozaj sa chcete vrátiť?"</string>
<string name="screen_create_poll_delete_option_a11y">"Odstrániť možnosť %1$s"</string>
<string name="screen_create_poll_question_desc">"Otázka alebo téma"</string>
<string name="screen_create_poll_question_hint">"O čom je anketa?"</string>
<string name="screen_create_poll_title">"Vytvoriť anketu"</string>
<string name="screen_edit_poll_delete_confirmation">"Ste si istý, že chcete odstrániť túto anketu?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Odstrániť anketu"</string>
<string name="screen_edit_poll_title">"Upraviť anketu"</string>
<string name="screen_polls_history_empty_ongoing">"Nepodarilo sa nájsť žiadne prebiehajúce ankety."</string>
<string name="screen_polls_history_empty_past">"Nie je možné nájsť žiadne predchádzajúce ankety."</string>
<string name="screen_polls_history_filter_ongoing">"Prebiehajúce"</string>
<string name="screen_polls_history_filter_past">"Minulé"</string>
<string name="screen_polls_history_title">"Ankety"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Lägg till alternativ"</string>
<string name="screen_create_poll_anonymous_desc">"Visa resultat först efter att omröstningen avslutats"</string>
<string name="screen_create_poll_anonymous_headline">"Dölj röster"</string>
<string name="screen_create_poll_answer_hint">"Alternativ %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Dina ändringar har inte sparats. Är du säker på att du vill gå tillbaka?"</string>
<string name="screen_create_poll_delete_option_a11y">"Radera alternativet %1$s"</string>
<string name="screen_create_poll_question_desc">"Fråga eller ämne"</string>
<string name="screen_create_poll_question_hint">"Vad handlar omröstningen om?"</string>
<string name="screen_create_poll_title">"Skapa omröstning"</string>
<string name="screen_edit_poll_delete_confirmation">"Är du säker på att du vill radera den här omröstningen?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Radera omröstning"</string>
<string name="screen_edit_poll_title">"Redigera omröstning"</string>
<string name="screen_polls_history_empty_ongoing">"Kan inte hitta några pågående omröstningar."</string>
<string name="screen_polls_history_empty_past">"Kan inte hitta några tidigare omröstningar."</string>
<string name="screen_polls_history_filter_ongoing">"Pågående"</string>
<string name="screen_polls_history_filter_past">"Tidigare"</string>
<string name="screen_polls_history_title">"Omröstningar"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Seçenek ekle"</string>
<string name="screen_create_poll_anonymous_desc">"Sonuçları yalnızca anket bittikten sonra göster"</string>
<string name="screen_create_poll_anonymous_headline">"Oyları gizle"</string>
<string name="screen_create_poll_answer_hint">"Seçenek %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Değişiklikleriniz kaydedilmedi. Geri dönmek istediğinden emin misin?"</string>
<string name="screen_create_poll_question_desc">"Soru veya konu"</string>
<string name="screen_create_poll_question_hint">"Anket ne hakkında?"</string>
<string name="screen_create_poll_title">"Anket Oluştur"</string>
<string name="screen_edit_poll_delete_confirmation">"Bu anketi silmek istediğinize emin misiniz?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Anketi Sil"</string>
<string name="screen_edit_poll_title">"Anketi düzenle"</string>
<string name="screen_polls_history_empty_ongoing">"Devam eden bir anket bulamadım."</string>
<string name="screen_polls_history_empty_past">"Geçmiş herhangi bir anket bulamıyorum."</string>
<string name="screen_polls_history_filter_ongoing">"Devam ediyor"</string>
<string name="screen_polls_history_filter_past">"Geçmiş"</string>
<string name="screen_polls_history_title">"Anketler"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Додати варіант"</string>
<string name="screen_create_poll_anonymous_desc">"Показувати результати тільки після закінчення опитування"</string>
<string name="screen_create_poll_anonymous_headline">"Приховати голоси"</string>
<string name="screen_create_poll_answer_hint">"Варіант %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Внесені зміни не збережено. Ви впевнені, що хочете повернутися?"</string>
<string name="screen_create_poll_delete_option_a11y">"Видалити варіант %1$s"</string>
<string name="screen_create_poll_question_desc">"Питання або тема"</string>
<string name="screen_create_poll_question_hint">"Про що йдеться в опитуванні?"</string>
<string name="screen_create_poll_title">"Створити опитування"</string>
<string name="screen_edit_poll_delete_confirmation">"Ви впевнені, що хочете видалити це опитування?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Видалити опитування"</string>
<string name="screen_edit_poll_title">"Редагувати опитування"</string>
<string name="screen_polls_history_empty_ongoing">"Не вдалося знайти жодних поточних опитувань."</string>
<string name="screen_polls_history_empty_past">"Не вдалося знайти жодних минулих опитувань."</string>
<string name="screen_polls_history_filter_ongoing">"Поточні"</string>
<string name="screen_polls_history_filter_past">"Минулі"</string>
<string name="screen_polls_history_title">"Опитування"</string>
</resources>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"اختیار شامل کریں"</string>
<string name="screen_create_poll_anonymous_desc">"رائے شماری کے بعد ہی نتائج ظاہر کریں"</string>
<string name="screen_create_poll_anonymous_headline">"آراء چھپائیں"</string>
<string name="screen_create_poll_answer_hint">"اختیار %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"آپ کی تبدیلیاں محفوظ نہیں کی گئیں۔ کیا آپ کو یقین ہے کہ آپ واپس جانا چاہتے ہیں؟"</string>
<string name="screen_create_poll_question_desc">"سوال یا موضوع"</string>
<string name="screen_create_poll_question_hint">"رائے شماری کس بارے میں ہے؟"</string>
<string name="screen_create_poll_title">"رائے شماری بنائیں"</string>
<string name="screen_edit_poll_delete_confirmation">"کیا آپ کو یقین ہے کہ آپ اس رائے شماری کو حذف کرنا چاہتے ہیں؟"</string>
<string name="screen_edit_poll_delete_confirmation_title">"رائے شماری حذف کریں"</string>
<string name="screen_edit_poll_title">"رائے شماری میں ترمیم کریں"</string>
<string name="screen_polls_history_empty_ongoing">"کوئی جاری رائے شماری ہا نہیں مل سکے۔"</string>
<string name="screen_polls_history_empty_past">"ماضی کے کوئی رائے شماری ہا نہیں مل سکے۔"</string>
<string name="screen_polls_history_filter_ongoing">"جاری"</string>
<string name="screen_polls_history_filter_past">"ماضی"</string>
<string name="screen_polls_history_title">"رائے شماری ہا"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"Variant qo\'shish"</string>
<string name="screen_create_poll_anonymous_desc">"Natijalarni faqat soʻrov tugagandan keyin koʻrsatish"</string>
<string name="screen_create_poll_anonymous_headline">"Ovozlarni yashirish"</string>
<string name="screen_create_poll_answer_hint">"Variant%1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"Oʻzgarishlar saqlanmadi. Haqiqatan ham orqaga qaytmoqchimisiz?"</string>
<string name="screen_create_poll_delete_option_a11y">"%1$s variantini ochirish"</string>
<string name="screen_create_poll_question_desc">"Savol yoki mavzu"</string>
<string name="screen_create_poll_question_hint">"So\'rovnoma nima haqida?"</string>
<string name="screen_create_poll_title">"Sorovnoma yaratish"</string>
<string name="screen_edit_poll_delete_confirmation">"Siz rostdan ham bu soʻrovnomani oʻchirib tashlamoqchimisiz?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"Sorovnomani ochirish"</string>
<string name="screen_edit_poll_title">"Sorovnomani tahrirlash"</string>
<string name="screen_polls_history_empty_ongoing">"Davom etayotgan soʻrovlar topilmadi."</string>
<string name="screen_polls_history_empty_past">"Avvalgi soʻrovnomalar topilmadi."</string>
<string name="screen_polls_history_filter_ongoing">"Jarayonda"</string>
<string name="screen_polls_history_filter_past">"Oʻtgan"</string>
<string name="screen_polls_history_title">"Soʻrovnomalar"</string>
</resources>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_create_poll_add_option_btn">"新增選項"</string>
<string name="screen_create_poll_anonymous_desc">"只在投票結束後顯示結果"</string>
<string name="screen_create_poll_anonymous_headline">"隱藏票數"</string>
<string name="screen_create_poll_answer_hint">"選項 %1$d"</string>
<string name="screen_create_poll_cancel_confirmation_content_android">"變更尚未儲存,您確定要返回嗎?"</string>
<string name="screen_create_poll_delete_option_a11y">"刪除選項 %1$s"</string>
<string name="screen_create_poll_question_desc">"問題或主題"</string>
<string name="screen_create_poll_question_hint">"投什麼?"</string>
<string name="screen_create_poll_title">"建立投票"</string>
<string name="screen_edit_poll_delete_confirmation">"您確定要刪除投票嗎?"</string>
<string name="screen_edit_poll_delete_confirmation_title">"刪除投票"</string>
<string name="screen_edit_poll_title">"編輯投票"</string>
<string name="screen_polls_history_empty_ongoing">"沒有進行中的投票。"</string>
<string name="screen_polls_history_empty_past">"沒有已結束的投票。"</string>
<string name="screen_polls_history_filter_ongoing">"進行中"</string>
<string name="screen_polls_history_filter_past">"已結束"</string>
<string name="screen_polls_history_title">"所有投票"</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More