First Commit
This commit is contained in:
@@ -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)
|
||||
}
|
||||
+16
@@ -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>
|
||||
}
|
||||
+20
@@ -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>
|
||||
}
|
||||
+27
@@ -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
|
||||
}
|
||||
+16
@@ -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
|
||||
}
|
||||
+13
@@ -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
|
||||
+32
@@ -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,
|
||||
)
|
||||
+204
@@ -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),
|
||||
)
|
||||
}
|
||||
+33
@@ -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,
|
||||
)
|
||||
+25
@@ -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
|
||||
}
|
||||
+100
@@ -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,
|
||||
)
|
||||
+302
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
+63
@@ -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">"C’est 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>
|
||||
@@ -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)
|
||||
}
|
||||
+16
@@ -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
|
||||
}
|
||||
+31
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -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
|
||||
}
|
||||
+21
@@ -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()
|
||||
}
|
||||
+68
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+244
@@ -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()
|
||||
}
|
||||
+36
@@ -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,
|
||||
)
|
||||
+177
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
+244
@@ -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,
|
||||
)
|
||||
+30
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
+122
@@ -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,
|
||||
)
|
||||
}
|
||||
)
|
||||
+110
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -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)
|
||||
}
|
||||
}
|
||||
+19
@@ -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
|
||||
}
|
||||
+84
@@ -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()
|
||||
}
|
||||
}
|
||||
+48
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+99
@@ -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)
|
||||
}
|
||||
}
|
||||
+29
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -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,
|
||||
)
|
||||
+265
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
+16
@@ -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),
|
||||
}
|
||||
+16
@@ -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,
|
||||
)
|
||||
+19
@@ -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
|
||||
}
|
||||
+67
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -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 n’ont pas été enregistrées. Êtes-vous certain de vouloir quitter ?"</string>
|
||||
<string name="screen_create_poll_delete_option_a11y">"Supprimer l’option %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 o‘chirish"</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">"So‘rovnoma 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">"So‘rovnomani o‘chirish"</string>
|
||||
<string name="screen_edit_poll_title">"So‘rovnomani 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
Reference in New Issue
Block a user