First Commit
This commit is contained in:
@@ -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>
|
||||
@@ -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">"Add option"</string>
|
||||
<string name="screen_create_poll_anonymous_desc">"Show results only after poll ends"</string>
|
||||
<string name="screen_create_poll_anonymous_headline">"Hide votes"</string>
|
||||
<string name="screen_create_poll_answer_hint">"Option %1$d"</string>
|
||||
<string name="screen_create_poll_cancel_confirmation_content_android">"Your changes have not been saved. Are you sure you want to go back?"</string>
|
||||
<string name="screen_create_poll_delete_option_a11y">"Delete option %1$s"</string>
|
||||
<string name="screen_create_poll_question_desc">"Question or topic"</string>
|
||||
<string name="screen_create_poll_question_hint">"What is the poll about?"</string>
|
||||
<string name="screen_create_poll_title">"Create Poll"</string>
|
||||
<string name="screen_edit_poll_delete_confirmation">"Are you sure you want to delete this poll?"</string>
|
||||
<string name="screen_edit_poll_delete_confirmation_title">"Delete Poll"</string>
|
||||
<string name="screen_edit_poll_title">"Edit poll"</string>
|
||||
<string name="screen_polls_history_empty_ongoing">"Can\'t find any ongoing polls."</string>
|
||||
<string name="screen_polls_history_empty_past">"Can\'t find any past polls."</string>
|
||||
<string name="screen_polls_history_filter_ongoing">"Ongoing"</string>
|
||||
<string name="screen_polls_history_filter_past">"Past"</string>
|
||||
<string name="screen_polls_history_title">"Polls"</string>
|
||||
</resources>
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.test.timeline.aPollContent
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
fun aPollTimelineItems(
|
||||
polls: Map<EventId, PollContent> = emptyMap(),
|
||||
): Flow<List<MatrixTimelineItem>> {
|
||||
return flowOf(
|
||||
polls.map { entry ->
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId(entry.key.value),
|
||||
event = anEventTimelineItem(
|
||||
eventId = entry.key,
|
||||
content = entry.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun anOngoingPollContent() = aPollContent(
|
||||
question = "Do you like polls?",
|
||||
answers = persistentListOf(
|
||||
PollAnswer("1", "Yes"),
|
||||
PollAnswer("2", "No"),
|
||||
PollAnswer("2", "Maybe"),
|
||||
),
|
||||
)
|
||||
|
||||
fun anEndedPollContent() = anOngoingPollContent().copy(
|
||||
endTime = 1702400215U
|
||||
)
|
||||
+576
@@ -0,0 +1,576 @@
|
||||
/*
|
||||
* 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 app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.TurbineTestContext
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Composer
|
||||
import im.vector.app.features.analytics.plan.PollCreation
|
||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||
import io.element.android.features.poll.api.create.CreatePollMode
|
||||
import io.element.android.features.poll.impl.aPollTimelineItems
|
||||
import io.element.android.features.poll.impl.anOngoingPollContent
|
||||
import io.element.android.features.poll.impl.data.PollRepository
|
||||
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.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class CreatePollPresenterTest {
|
||||
@get:Rule val warmUpRule = WarmUpRule()
|
||||
|
||||
private val pollEventId = AN_EVENT_ID
|
||||
private var navUpInvocationsCount = 0
|
||||
private val existingPoll = anOngoingPollContent()
|
||||
private val timeline = FakeTimeline(
|
||||
timelineItems = aPollTimelineItems(mapOf(pollEventId to existingPoll))
|
||||
)
|
||||
private val fakeJoinedRoom = FakeJoinedRoom(
|
||||
liveTimeline = timeline
|
||||
)
|
||||
private val fakeAnalyticsService = FakeAnalyticsService()
|
||||
private val fakeMessageComposerContext = FakeMessageComposerContext()
|
||||
|
||||
@Test
|
||||
fun `default state has proper default values`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in edit mode, poll values are loaded`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID))
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest {
|
||||
val room = FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline()
|
||||
)
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
assertThat(fakeAnalyticsService.trackedErrors.filterIsInstance<CreatePollException.GetPollFailed>()).isNotEmpty()
|
||||
assertThat(navUpInvocationsCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non blank question and 2 answers are required to create a poll`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
assertThat(initial.canSave).isFalse()
|
||||
|
||||
initial.eventSink(CreatePollEvents.SetQuestion("A question?"))
|
||||
val questionSet = awaitItem()
|
||||
assertThat(questionSet.canSave).isFalse()
|
||||
|
||||
questionSet.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1"))
|
||||
val answer1Set = awaitItem()
|
||||
assertThat(answer1Set.canSave).isFalse()
|
||||
|
||||
answer1Set.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2"))
|
||||
val answer2Set = awaitItem()
|
||||
assertThat(answer2Set.canSave).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create poll sends a poll start event`() = runTest {
|
||||
val createPollResult = lambdaRecorder<String, List<String>, Int, PollKind, Result<Unit>> { _, _, _, _ -> Result.success(Unit) }
|
||||
val presenter = createCreatePollPresenter(
|
||||
room = FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline().apply {
|
||||
createPollLambda = createPollResult
|
||||
},
|
||||
),
|
||||
mode = CreatePollMode.NewPoll,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetQuestion("A question?"))
|
||||
initial.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1"))
|
||||
initial.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2"))
|
||||
skipItems(3)
|
||||
initial.eventSink(CreatePollEvents.Save)
|
||||
delay(1) // Wait for the coroutine to finish
|
||||
createPollResult.assertions().isCalledOnce()
|
||||
.with(
|
||||
value("A question?"),
|
||||
value(listOf("Answer 1", "Answer 2")),
|
||||
value(1),
|
||||
value(PollKind.Disclosed),
|
||||
)
|
||||
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2)
|
||||
assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo(
|
||||
Composer(
|
||||
inThread = false,
|
||||
isEditing = false,
|
||||
isReply = false,
|
||||
messageType = Composer.MessageType.Poll,
|
||||
)
|
||||
)
|
||||
assertThat(fakeAnalyticsService.capturedEvents[1]).isEqualTo(
|
||||
PollCreation(
|
||||
action = PollCreation.Action.Create,
|
||||
isUndisclosed = false,
|
||||
numberOfAnswers = 2,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when poll creation fails, error is tracked`() = runTest {
|
||||
val error = Exception("cause")
|
||||
val createPollResult = lambdaRecorder<String, List<String>, Int, PollKind, Result<Unit>> { _, _, _, _ ->
|
||||
Result.failure(error)
|
||||
}
|
||||
val presenter = createCreatePollPresenter(
|
||||
room = FakeJoinedRoom(
|
||||
liveTimeline = FakeTimeline().apply {
|
||||
createPollLambda = createPollResult
|
||||
},
|
||||
),
|
||||
mode = CreatePollMode.NewPoll,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem().eventSink(CreatePollEvents.SetQuestion("A question?"))
|
||||
awaitItem().eventSink(CreatePollEvents.SetAnswer(0, "Answer 1"))
|
||||
awaitItem().eventSink(CreatePollEvents.SetAnswer(1, "Answer 2"))
|
||||
awaitItem().eventSink(CreatePollEvents.Save)
|
||||
delay(1) // Wait for the coroutine to finish
|
||||
createPollResult.assertions().isCalledOnce()
|
||||
assertThat(fakeAnalyticsService.capturedEvents).isEmpty()
|
||||
assertThat(fakeAnalyticsService.trackedErrors).hasSize(1)
|
||||
assertThat(fakeAnalyticsService.trackedErrors).containsExactly(
|
||||
CreatePollException.SavePollFailed("Failed to create poll", error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `edit poll sends a poll edit event`() = runTest {
|
||||
val editPollLambda = lambdaRecorder { _: EventId, _: String, _: List<String>, _: Int, _: PollKind ->
|
||||
Result.success(Unit)
|
||||
}
|
||||
timeline.apply {
|
||||
this.editPollLambda = editPollLambda
|
||||
}
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded().apply {
|
||||
eventSink(CreatePollEvents.SetQuestion("Changed question"))
|
||||
}
|
||||
awaitItem().apply {
|
||||
eventSink(CreatePollEvents.SetAnswer(0, "Changed answer 1"))
|
||||
}
|
||||
awaitItem().apply {
|
||||
eventSink(CreatePollEvents.SetAnswer(1, "Changed answer 2"))
|
||||
}
|
||||
awaitPollLoaded(
|
||||
newQuestion = "Changed question",
|
||||
newAnswer1 = "Changed answer 1",
|
||||
newAnswer2 = "Changed answer 2",
|
||||
).apply {
|
||||
eventSink(CreatePollEvents.Save)
|
||||
}
|
||||
advanceUntilIdle() // Wait for the coroutine to finish
|
||||
|
||||
assert(editPollLambda)
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
value(pollEventId),
|
||||
value("Changed question"),
|
||||
value(listOf("Changed answer 1", "Changed answer 2", "Maybe")),
|
||||
value(1),
|
||||
value(PollKind.Disclosed)
|
||||
)
|
||||
|
||||
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2)
|
||||
assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo(
|
||||
Composer(
|
||||
inThread = false,
|
||||
isEditing = true,
|
||||
isReply = false,
|
||||
messageType = Composer.MessageType.Poll,
|
||||
)
|
||||
)
|
||||
assertThat(fakeAnalyticsService.capturedEvents[1]).isEqualTo(
|
||||
PollCreation(
|
||||
action = PollCreation.Action.Edit,
|
||||
isUndisclosed = false,
|
||||
numberOfAnswers = 3,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when edit poll fails, error is tracked`() = runTest {
|
||||
val error = Exception("cause")
|
||||
val presenter = createCreatePollPresenter(
|
||||
room = FakeJoinedRoom(
|
||||
liveTimeline = timeline,
|
||||
),
|
||||
mode = CreatePollMode.EditPoll(pollEventId),
|
||||
)
|
||||
val editPollLambda = lambdaRecorder { _: EventId, _: String, _: List<String>, _: Int, _: PollKind ->
|
||||
Result.failure<Unit>(error)
|
||||
}
|
||||
timeline.apply {
|
||||
this.editPollLambda = editPollLambda
|
||||
}
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded().eventSink(CreatePollEvents.SetAnswer(0, "A"))
|
||||
awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvents.Save)
|
||||
advanceUntilIdle() // Wait for the coroutine to finish
|
||||
editPollLambda.assertions().isCalledOnce()
|
||||
assertThat(fakeAnalyticsService.capturedEvents).isEmpty()
|
||||
assertThat(fakeAnalyticsService.trackedErrors).hasSize(1)
|
||||
assertThat(fakeAnalyticsService.trackedErrors).containsExactly(
|
||||
CreatePollException.SavePollFailed("Failed to edit poll", error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add answer button adds an empty answer and removing it removes it`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
assertThat(initial.answers.size).isEqualTo(2)
|
||||
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
val answerAdded = awaitItem()
|
||||
assertThat(answerAdded.answers.size).isEqualTo(3)
|
||||
assertThat(answerAdded.answers[2].text).isEmpty()
|
||||
|
||||
initial.eventSink(CreatePollEvents.RemoveAnswer(2))
|
||||
val answerRemoved = awaitItem()
|
||||
assertThat(answerRemoved.answers.size).isEqualTo(2)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `set question sets the question`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetQuestion("A question?"))
|
||||
val questionSet = awaitItem()
|
||||
assertThat(questionSet.question).isEqualTo("A question?")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `set poll answer sets the given poll answer`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetAnswer(0, "This is answer 1"))
|
||||
val answerSet = awaitItem()
|
||||
assertThat(answerSet.answers.first().text).isEqualTo("This is answer 1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `set poll kind sets the poll kind`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetPollKind(PollKind.Undisclosed))
|
||||
val kindSet = awaitItem()
|
||||
assertThat(kindSet.pollKind).isEqualTo(PollKind.Undisclosed)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can add options when between 2 and 20 and then no more`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
assertThat(initial.canAddAnswer).isTrue()
|
||||
repeat(17) {
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
assertThat(awaitItem().canAddAnswer).isTrue()
|
||||
}
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
assertThat(awaitItem().canAddAnswer).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can delete option if there are more than 2`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
assertThat(initial.answers.all { it.canDelete }).isFalse()
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
assertThat(awaitItem().answers.all { it.canDelete }).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `option with more than 240 char is truncated`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetAnswer(0, "A".repeat(241)))
|
||||
assertThat(awaitItem().answers.first().text.length).isEqualTo(240)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `navBack event calls navBack lambda`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
initial.eventSink(CreatePollEvents.NavBack)
|
||||
assertThat(navUpInvocationsCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirm nav back from new poll with blank fields calls nav back lambda`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
assertThat(initial.showBackConfirmation).isFalse()
|
||||
initial.eventSink(CreatePollEvents.ConfirmNavBack)
|
||||
assertThat(navUpInvocationsCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirm nav back from new poll with non blank fields shows confirmation dialog and cancelling hides it`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetQuestion("Non blank"))
|
||||
assertThat(awaitItem().showBackConfirmation).isFalse()
|
||||
initial.eventSink(CreatePollEvents.ConfirmNavBack)
|
||||
assertThat(awaitItem().showBackConfirmation).isTrue()
|
||||
initial.eventSink(CreatePollEvents.HideConfirmation)
|
||||
assertThat(awaitItem().showBackConfirmation).isFalse()
|
||||
assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirm nav back from existing poll with unchanged fields calls nav back lambda`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
val loaded = awaitPollLoaded()
|
||||
assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
assertThat(loaded.showBackConfirmation).isFalse()
|
||||
loaded.eventSink(CreatePollEvents.ConfirmNavBack)
|
||||
assertThat(navUpInvocationsCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirm nav back from existing poll with changed fields shows confirmation dialog and cancelling hides it`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
val loaded = awaitPollLoaded()
|
||||
loaded.eventSink(CreatePollEvents.SetQuestion("CHANGED"))
|
||||
assertThat(awaitItem().showBackConfirmation).isFalse()
|
||||
loaded.eventSink(CreatePollEvents.ConfirmNavBack)
|
||||
assertThat(awaitItem().showBackConfirmation).isTrue()
|
||||
loaded.eventSink(CreatePollEvents.HideConfirmation)
|
||||
assertThat(awaitItem().showBackConfirmation).isFalse()
|
||||
assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete confirms`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
|
||||
val redactEventLambda = lambdaRecorder { _: EventOrTransactionId, _: String? -> Result.success(Unit) }
|
||||
timeline.redactEventLambda = redactEventLambda
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
|
||||
awaitDeleteConfirmation()
|
||||
assert(redactEventLambda).isNeverCalled()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete can be cancelled`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
|
||||
val redactEventLambda = lambdaRecorder { _: EventOrTransactionId, _: String? -> Result.success(Unit) }
|
||||
timeline.redactEventLambda = redactEventLambda
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
|
||||
awaitDeleteConfirmation().eventSink(CreatePollEvents.HideConfirmation)
|
||||
awaitPollLoaded().apply {
|
||||
assertThat(showDeleteConfirmation).isFalse()
|
||||
}
|
||||
assert(redactEventLambda).isNeverCalled()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete can be confirmed`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
|
||||
val redactEventLambda = lambdaRecorder { _: EventOrTransactionId, _: String? -> Result.success(Unit) }
|
||||
timeline.redactEventLambda = redactEventLambda
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
|
||||
awaitDeleteConfirmation().eventSink(CreatePollEvents.Delete(confirmed = true))
|
||||
awaitPollLoaded().apply {
|
||||
assertThat(showDeleteConfirmation).isFalse()
|
||||
}
|
||||
assert(redactEventLambda)
|
||||
.isCalledOnce()
|
||||
.with(value(pollEventId.toEventOrTransactionId()), any())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TurbineTestContext<CreatePollState>.awaitDefaultItem() =
|
||||
awaitItem().apply {
|
||||
assertThat(canSave).isFalse()
|
||||
assertThat(canAddAnswer).isTrue()
|
||||
assertThat(question).isEmpty()
|
||||
assertThat(answers).isEqualTo(listOf(Answer("", false), Answer("", false)))
|
||||
assertThat(pollKind).isEqualTo(PollKind.Disclosed)
|
||||
assertThat(showBackConfirmation).isFalse()
|
||||
assertThat(showDeleteConfirmation).isFalse()
|
||||
}
|
||||
|
||||
private suspend fun TurbineTestContext<CreatePollState>.awaitDeleteConfirmation() =
|
||||
awaitItem().apply {
|
||||
assertThat(showDeleteConfirmation).isTrue()
|
||||
}
|
||||
|
||||
private suspend fun TurbineTestContext<CreatePollState>.awaitPollLoaded(
|
||||
newQuestion: String? = null,
|
||||
newAnswer1: String? = null,
|
||||
newAnswer2: String? = null,
|
||||
) =
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.canSave).isTrue()
|
||||
assertThat(state.canAddAnswer).isTrue()
|
||||
assertThat(state.question).isEqualTo(newQuestion ?: existingPoll.question)
|
||||
assertThat(state.answers).isEqualTo(existingPoll.expectedAnswersState().toMutableList().apply {
|
||||
newAnswer1?.let { this[0] = Answer(it, true) }
|
||||
newAnswer2?.let { this[1] = Answer(it, true) }
|
||||
})
|
||||
assertThat(state.pollKind).isEqualTo(existingPoll.kind)
|
||||
}
|
||||
|
||||
private fun createCreatePollPresenter(
|
||||
mode: CreatePollMode = CreatePollMode.NewPoll,
|
||||
room: FakeJoinedRoom = fakeJoinedRoom,
|
||||
timelineMode: Timeline.Mode = Timeline.Mode.Live,
|
||||
): CreatePollPresenter = CreatePollPresenter(
|
||||
repositoryFactory = object : PollRepository.Factory {
|
||||
override fun create(timelineMode: Timeline.Mode): PollRepository {
|
||||
return PollRepository(room, LiveTimelineProvider(room), timelineMode)
|
||||
}
|
||||
},
|
||||
analyticsService = fakeAnalyticsService,
|
||||
messageComposerContext = fakeMessageComposerContext,
|
||||
navigateUp = { navUpInvocationsCount++ },
|
||||
mode = mode,
|
||||
timelineMode = timelineMode,
|
||||
)
|
||||
}
|
||||
|
||||
private fun PollContent.expectedAnswersState() = answers.map { answer ->
|
||||
Answer(
|
||||
text = answer.text,
|
||||
canDelete = answers.size > 2,
|
||||
)
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 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.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||
import io.element.android.features.poll.api.create.CreatePollMode
|
||||
import io.element.android.features.poll.impl.data.PollRepository
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.timeline.LiveTimelineProvider
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultCreatePollEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() {
|
||||
val entryPoint = DefaultCreatePollEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
CreatePollNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
presenterFactory = { timelineMode: Timeline.Mode, backNavigator: () -> Unit, mode: CreatePollMode ->
|
||||
CreatePollPresenter(
|
||||
repositoryFactory = {
|
||||
val room = FakeJoinedRoom()
|
||||
PollRepository(room, LiveTimelineProvider(room), timelineMode)
|
||||
},
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
messageComposerContext = FakeMessageComposerContext(),
|
||||
navigateUp = backNavigator,
|
||||
mode = mode,
|
||||
timelineMode = timelineMode,
|
||||
)
|
||||
},
|
||||
analyticsService = FakeAnalyticsService(),
|
||||
)
|
||||
}
|
||||
val params = CreatePollEntryPoint.Params(
|
||||
timelineMode = Timeline.Mode.Live,
|
||||
mode = CreatePollMode.NewPoll,
|
||||
)
|
||||
val result = entryPoint.createNode(
|
||||
parentNode = parentNode,
|
||||
buildContext = BuildContext.root(null),
|
||||
params = params,
|
||||
)
|
||||
assertThat(result).isInstanceOf(CreatePollNode::class.java)
|
||||
assertThat(result.plugins).contains(CreatePollNode.Inputs(timelineMode = params.timelineMode, mode = params.mode))
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.SaverScope
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.Test
|
||||
|
||||
class PollFormStateSaverTest {
|
||||
companion object {
|
||||
val CanSaveScope = SaverScope { true }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test save and restore`() {
|
||||
val state = PollFormState(
|
||||
question = "question",
|
||||
answers = persistentListOf("answer1", "answer2"),
|
||||
isDisclosed = true,
|
||||
)
|
||||
|
||||
val saved = with(CanSaveScope) {
|
||||
with(pollFormStateSaver) {
|
||||
save(state)
|
||||
}
|
||||
}
|
||||
|
||||
val restored = saved?.let {
|
||||
pollFormStateSaver.restore(it)
|
||||
}
|
||||
|
||||
assertThat(restored).isEqualTo(state)
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.poll.impl.PollConstants
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.Test
|
||||
|
||||
class PollFormStateTest {
|
||||
@Test
|
||||
fun `with new answer`() {
|
||||
val state = PollFormState.Empty
|
||||
val newState = state.withNewAnswer()
|
||||
assertThat(newState.answers).isEqualTo(listOf("", "", ""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `with new answer, given cannot add, doesn't add`() {
|
||||
val state = PollFormState.Empty.withBlankAnswers(PollConstants.MAX_ANSWERS)
|
||||
val newState = state.withNewAnswer()
|
||||
assertThat(newState).isEqualTo(state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `with answer deleted, given cannot delete, doesn't delete`() {
|
||||
val state = PollFormState.Empty
|
||||
val newState = state.withAnswerRemoved(0)
|
||||
assertThat(newState).isEqualTo(state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `with answer deleted, given can delete`() {
|
||||
val state = PollFormState.Empty.withNewAnswer()
|
||||
val newState = state.withAnswerRemoved(0)
|
||||
assertThat(newState).isEqualTo(PollFormState.Empty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `with answer changed`() {
|
||||
val state = PollFormState.Empty
|
||||
val newState = state.withAnswerChanged(1, "New answer")
|
||||
assertThat(newState).isEqualTo(PollFormState.Empty.copy(
|
||||
answers = listOf("", "New answer").toImmutableList()
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `with answer changed, given it is too long, truncates`() {
|
||||
val tooLongAnswer = "a".repeat(PollConstants.MAX_ANSWER_LENGTH * 2)
|
||||
val truncatedAnswer = "a".repeat(PollConstants.MAX_ANSWER_LENGTH)
|
||||
val state = PollFormState.Empty
|
||||
val newState = state.withAnswerChanged(1, tooLongAnswer)
|
||||
assertThat(newState).isEqualTo(PollFormState.Empty.copy(
|
||||
answers = listOf("", truncatedAnswer).toImmutableList()
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can add answer is true when it does not have max answers`() {
|
||||
val state = PollFormState.Empty.withBlankAnswers(PollConstants.MAX_ANSWERS - 1)
|
||||
assertThat(state.canAddAnswer).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can add answer is false when it has max answers`() {
|
||||
val state = PollFormState.Empty.withBlankAnswers(PollConstants.MAX_ANSWERS)
|
||||
assertThat(state.canAddAnswer).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can delete answer is false when it has min answers`() {
|
||||
val state = PollFormState.Empty.withBlankAnswers(PollConstants.MIN_ANSWERS)
|
||||
assertThat(state.canDeleteAnswer).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can delete answer is true when it has more than min answers`() {
|
||||
val numAnswers = PollConstants.MIN_ANSWERS + 1
|
||||
val state = PollFormState.Empty.withBlankAnswers(numAnswers)
|
||||
assertThat(state.canDeleteAnswer).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `is valid is true when it is valid`() {
|
||||
val state = aValidPollFormState()
|
||||
assertThat(state.isValid).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `is valid is false when question is blank`() {
|
||||
val state = aValidPollFormState().copy(question = "")
|
||||
assertThat(state.isValid).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `is valid is false when not enough answers`() {
|
||||
val state = aValidPollFormState().copy(answers = listOf("").toImmutableList())
|
||||
assertThat(state.isValid).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `is valid is false when one answer is blank`() {
|
||||
val state = aValidPollFormState().withNewAnswer()
|
||||
assertThat(state.isValid).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `poll kind when is disclosed`() {
|
||||
val state = PollFormState.Empty.copy(isDisclosed = true)
|
||||
assertThat(state.pollKind).isEqualTo(PollKind.Disclosed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `poll kind when is not disclosed`() {
|
||||
val state = PollFormState.Empty.copy(isDisclosed = false)
|
||||
assertThat(state.pollKind).isEqualTo(PollKind.Undisclosed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun aValidPollFormState(): PollFormState {
|
||||
return PollFormState.Empty.copy(
|
||||
question = "question",
|
||||
answers = listOf("answer1", "answer2").toImmutableList(),
|
||||
isDisclosed = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun PollFormState.withBlankAnswers(numAnswers: Int): PollFormState =
|
||||
copy(answers = List(numAnswers) { "" }.toImmutableList())
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 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.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.poll.test.create.FakeCreatePollEntryPoint
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultPollHistoryEntryPointTest {
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
@Test
|
||||
fun `test node builder`() = runTest {
|
||||
val entryPoint = DefaultPollHistoryEntryPoint()
|
||||
val parentNode = TestParentNode.create { buildContext, plugins ->
|
||||
PollHistoryFlowNode(
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
createPollEntryPoint = FakeCreatePollEntryPoint(),
|
||||
)
|
||||
}
|
||||
val result = entryPoint.createNode(parentNode, BuildContext.root(null))
|
||||
assertThat(result).isInstanceOf(PollHistoryFlowNode::class.java)
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* 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 app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.aPollTimelineItems
|
||||
import io.element.android.features.poll.impl.anEndedPollContent
|
||||
import io.element.android.features.poll.impl.anOngoingPollContent
|
||||
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
|
||||
import io.element.android.features.poll.impl.history.model.PollHistoryItemsFactory
|
||||
import io.element.android.features.poll.impl.model.DefaultPollContentStateFactory
|
||||
import io.element.android.features.poll.test.actions.FakeEndPollAction
|
||||
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
|
||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class PollHistoryPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private val backwardPaginationStatus = MutableStateFlow(Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true))
|
||||
private val timeline = FakeTimeline(
|
||||
timelineItems = aPollTimelineItems(
|
||||
mapOf(
|
||||
AN_EVENT_ID to anOngoingPollContent(),
|
||||
AN_EVENT_ID_2 to anEndedPollContent()
|
||||
)
|
||||
),
|
||||
backwardPaginationStatus = backwardPaginationStatus
|
||||
)
|
||||
private val room = FakeJoinedRoom(
|
||||
liveTimeline = timeline
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `present - initial states`() = runTest {
|
||||
val presenter = createPollHistoryPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING)
|
||||
assertThat(state.pollHistoryItems.size).isEqualTo(0)
|
||||
assertThat(state.isLoading).isTrue()
|
||||
assertThat(state.hasMoreToLoad).isTrue()
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.pollHistoryItems.size).isEqualTo(2)
|
||||
assertThat(state.pollHistoryItems.ongoing).hasSize(1)
|
||||
assertThat(state.pollHistoryItems.past).hasSize(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - change filter scenario`() = runTest {
|
||||
val presenter = createPollHistoryPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING)
|
||||
state.eventSink(PollHistoryEvents.SelectFilter(PollHistoryFilter.PAST))
|
||||
}
|
||||
skipItems(1)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.PAST)
|
||||
state.eventSink(PollHistoryEvents.SelectFilter(PollHistoryFilter.ONGOING))
|
||||
}
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - poll actions scenario`() = runTest {
|
||||
val sendPollResponseAction = FakeSendPollResponseAction()
|
||||
val endPollAction = FakeEndPollAction()
|
||||
val presenter = createPollHistoryPresenter(
|
||||
sendPollResponseAction = sendPollResponseAction,
|
||||
endPollAction = endPollAction
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val state = awaitItem()
|
||||
state.eventSink(PollHistoryEvents.EndPoll(AN_EVENT_ID))
|
||||
runCurrent()
|
||||
endPollAction.verifyExecutionCount(1)
|
||||
state.eventSink(PollHistoryEvents.SelectPollAnswer(AN_EVENT_ID, "answer"))
|
||||
runCurrent()
|
||||
sendPollResponseAction.verifyExecutionCount(1)
|
||||
cancelAndConsumeRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - load more scenario`() = runTest {
|
||||
val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection ->
|
||||
Result.success(false)
|
||||
}
|
||||
timeline.apply {
|
||||
this.paginateLambda = paginateLambda
|
||||
}
|
||||
val presenter = createPollHistoryPresenter(room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
val loadedState = awaitItem()
|
||||
assertThat(loadedState.isLoading).isFalse()
|
||||
loadedState.eventSink(PollHistoryEvents.LoadMore)
|
||||
backwardPaginationStatus.getAndUpdate { it.copy(isPaginating = true) }
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isLoading).isTrue()
|
||||
}
|
||||
backwardPaginationStatus.getAndUpdate { it.copy(isPaginating = false) }
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.isLoading).isFalse()
|
||||
}
|
||||
// Called once by the initial load and once by the load more event
|
||||
assert(paginateLambda).isCalledExactly(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TestScope.createPollHistoryPresenter(
|
||||
room: FakeJoinedRoom = FakeJoinedRoom(),
|
||||
endPollAction: EndPollAction = FakeEndPollAction(),
|
||||
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
|
||||
pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory(
|
||||
pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()),
|
||||
dateFormatter = FakeDateFormatter(),
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
),
|
||||
): PollHistoryPresenter {
|
||||
return PollHistoryPresenter(
|
||||
sessionCoroutineScope = this,
|
||||
sendPollResponseAction = sendPollResponseAction,
|
||||
endPollAction = endPollAction,
|
||||
pollHistoryItemFactory = pollHistoryItemFactory,
|
||||
room = room,
|
||||
)
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* 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.impl.history
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.poll.api.pollcontent.aPollContentState
|
||||
import io.element.android.features.poll.impl.R
|
||||
import io.element.android.features.poll.impl.history.model.PollHistoryFilter
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PollHistoryViewTest {
|
||||
@get:Rule
|
||||
val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on back invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>(expectEvents = false)
|
||||
ensureCalledOnce {
|
||||
rule.setPollHistoryViewView(
|
||||
aPollHistoryState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
goBack = it
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on edit poll invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>(expectEvents = false)
|
||||
val eventId = EventId("\$anEventId")
|
||||
val state = aPollHistoryState(
|
||||
currentItems = listOf(
|
||||
aPollHistoryItem(
|
||||
state = aPollContentState(
|
||||
eventId = eventId,
|
||||
isMine = true,
|
||||
isEnded = false,
|
||||
)
|
||||
)
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
ensureCalledOnceWithParam(eventId) {
|
||||
rule.setPollHistoryViewView(
|
||||
state = state,
|
||||
onEditPoll = it
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_edit_poll)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on poll end emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>()
|
||||
val eventId = EventId("\$anEventId")
|
||||
val state = aPollHistoryState(
|
||||
currentItems = listOf(
|
||||
aPollHistoryItem(
|
||||
state = aPollContentState(
|
||||
eventId = eventId,
|
||||
isMine = true,
|
||||
isEnded = false,
|
||||
isPollEditable = false,
|
||||
)
|
||||
)
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
rule.setPollHistoryViewView(
|
||||
state = state,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_end_poll)
|
||||
// Cancel the dialog
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
// Do it again, and confirm the dialog
|
||||
rule.clickOn(CommonStrings.action_end_poll)
|
||||
eventsRecorder.assertEmpty()
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventsRecorder.assertSingle(
|
||||
PollHistoryEvents.EndPoll(eventId)
|
||||
)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on poll answer emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>()
|
||||
val eventId = EventId("\$anEventId")
|
||||
val state = aPollHistoryState(
|
||||
currentItems = listOf(
|
||||
aPollHistoryItem(
|
||||
state = aPollContentState(
|
||||
eventId = eventId,
|
||||
isMine = true,
|
||||
isEnded = false,
|
||||
isPollEditable = false,
|
||||
)
|
||||
)
|
||||
),
|
||||
eventSink = eventsRecorder
|
||||
)
|
||||
val answer = state.pollHistoryItems.ongoing.first().state.answerItems.first().answer
|
||||
rule.setPollHistoryViewView(
|
||||
state = state,
|
||||
)
|
||||
rule.onNodeWithText(
|
||||
text = answer.text,
|
||||
useUnmergedTree = true,
|
||||
).performClick()
|
||||
eventsRecorder.assertSingle(
|
||||
PollHistoryEvents.SelectPollAnswer(eventId, answer.id)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on past tab emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>()
|
||||
rule.setPollHistoryViewView(
|
||||
aPollHistoryState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_polls_history_filter_past)
|
||||
eventsRecorder.assertSingle(
|
||||
PollHistoryEvents.SelectFilter(filter = PollHistoryFilter.PAST)
|
||||
)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `clicking on load more emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<PollHistoryEvents>()
|
||||
rule.setPollHistoryViewView(
|
||||
aPollHistoryState(
|
||||
hasMoreToLoad = true,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_load_more)
|
||||
eventsRecorder.assertSingle(
|
||||
PollHistoryEvents.LoadMore
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPollHistoryViewView(
|
||||
state: PollHistoryState,
|
||||
onEditPoll: (EventId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
goBack: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
PollHistoryView(
|
||||
state = state,
|
||||
onEditPoll = onEditPoll,
|
||||
goBack = goBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* 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.pollcontent
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.poll.api.pollcontent.PollAnswerItem
|
||||
import io.element.android.features.poll.api.pollcontent.PollContentState
|
||||
import io.element.android.features.poll.impl.model.DefaultPollContentStateFactory
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
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.timeline.item.event.PollContent
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_10
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_4
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_5
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_6
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_7
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_8
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_9
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class PollContentStateFactoryTest {
|
||||
private val factory = DefaultPollContentStateFactory(FakeMatrixClient())
|
||||
private val eventTimelineItem = anEventTimelineItem()
|
||||
|
||||
@Test
|
||||
fun `Disclosed poll - not ended, no votes`() = runTest {
|
||||
val state = factory.create(eventTimelineItem, aPollContent())
|
||||
val expectedState = aPollContentState()
|
||||
assertThat(state).isEqualTo(expectedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest {
|
||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
val state = factory.create(
|
||||
eventTimelineItem,
|
||||
aPollContent(votes = votes)
|
||||
)
|
||||
val expectedState = aPollContentState(
|
||||
answerItems = listOf(
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, votesCount = 3, percentage = 0.3f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, votesCount = 6, percentage = 0.6f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_3),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, votesCount = 1, percentage = 0.1f),
|
||||
)
|
||||
)
|
||||
assertThat(state).isEqualTo(expectedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Disclosed poll - ended, no votes, no winner`() = runTest {
|
||||
val state = factory.create(eventTimelineItem, aPollContent(endTime = 1UL))
|
||||
val expectedState = aPollContentState().let {
|
||||
it.copy(
|
||||
answerItems = it.answerItems.map { answerItem -> answerItem.copy(isEnabled = false) }.toImmutableList(),
|
||||
isPollEnded = true,
|
||||
)
|
||||
}
|
||||
assertThat(state).isEqualTo(expectedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
val state = factory.create(
|
||||
eventTimelineItem,
|
||||
aPollContent(votes = votes, endTime = 1UL)
|
||||
)
|
||||
val expectedState = aPollContentState(
|
||||
answerItems = listOf(
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f),
|
||||
),
|
||||
isEnded = true,
|
||||
)
|
||||
assertThat(state).isEqualTo(expectedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest {
|
||||
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
val state = factory.create(
|
||||
eventTimelineItem,
|
||||
aPollContent(votes = votes, endTime = 1UL)
|
||||
)
|
||||
val expectedState = aPollContentState(
|
||||
answerItems = listOf(
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
||||
),
|
||||
isEnded = true,
|
||||
)
|
||||
assertThat(state).isEqualTo(expectedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Undisclosed poll - not ended, no votes`() = runTest {
|
||||
val state = factory.create(eventTimelineItem, aPollContent(PollKind.Undisclosed))
|
||||
val expectedState = aPollContentState(pollKind = PollKind.Undisclosed).let {
|
||||
it.copy(
|
||||
answerItems = it.answerItems.map { answerItem -> answerItem.copy(showVotes = false) }.toImmutableList()
|
||||
)
|
||||
}
|
||||
assertThat(state).isEqualTo(expectedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest {
|
||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
val state = factory.create(
|
||||
eventTimelineItem,
|
||||
aPollContent(PollKind.Undisclosed, votes = votes)
|
||||
)
|
||||
val expectedState = aPollContentState(
|
||||
pollKind = PollKind.Undisclosed,
|
||||
answerItems = listOf(
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, showVotes = false, votesCount = 3, percentage = 0.3f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, showVotes = false, isSelected = true, votesCount = 6, percentage = 0.6f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_3, showVotes = false),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, showVotes = false, votesCount = 1, percentage = 0.1f),
|
||||
),
|
||||
)
|
||||
assertThat(state).isEqualTo(expectedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Undisclosed poll - ended, no votes, no winner`() = runTest {
|
||||
val state = factory.create(eventTimelineItem, aPollContent(PollKind.Undisclosed, endTime = 1UL))
|
||||
val expectedState = aPollContentState(
|
||||
isEnded = true,
|
||||
pollKind = PollKind.Undisclosed
|
||||
).let {
|
||||
it.copy(
|
||||
answerItems = it.answerItems.map { answerItem -> answerItem.copy(showVotes = true, isEnabled = false) }.toImmutableList(),
|
||||
)
|
||||
}
|
||||
assertThat(state).isEqualTo(expectedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest {
|
||||
val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
val state = factory.create(
|
||||
eventTimelineItem,
|
||||
aPollContent(PollKind.Undisclosed, votes = votes, endTime = 1UL)
|
||||
)
|
||||
val expectedState = aPollContentState(
|
||||
pollKind = PollKind.Undisclosed,
|
||||
answerItems = listOf(
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f),
|
||||
),
|
||||
isEnded = true,
|
||||
)
|
||||
assertThat(state).isEqualTo(expectedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest {
|
||||
val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap()
|
||||
val state = factory.create(
|
||||
eventTimelineItem,
|
||||
aPollContent(PollKind.Undisclosed, votes = votes, endTime = 1UL)
|
||||
)
|
||||
val expectedState = aPollContentState(
|
||||
pollKind = PollKind.Undisclosed,
|
||||
answerItems = listOf(
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false),
|
||||
aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f),
|
||||
),
|
||||
isEnded = true,
|
||||
)
|
||||
assertThat(state).isEqualTo(expectedState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `eventId is populated`() = runTest {
|
||||
val state = factory.create(eventTimelineItem, aPollContent())
|
||||
assertThat(state.eventId).isEqualTo(eventTimelineItem.eventId)
|
||||
}
|
||||
|
||||
private fun aPollContent(
|
||||
pollKind: PollKind = PollKind.Disclosed,
|
||||
votes: ImmutableMap<String, ImmutableList<UserId>> = persistentMapOf(),
|
||||
endTime: ULong? = null,
|
||||
): PollContent = PollContent(
|
||||
question = A_POLL_QUESTION,
|
||||
kind = pollKind,
|
||||
maxSelections = 1UL,
|
||||
answers = persistentListOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4),
|
||||
votes = votes,
|
||||
endTime = endTime,
|
||||
isEdited = false,
|
||||
)
|
||||
|
||||
private fun aPollContentState(
|
||||
eventId: EventId? = AN_EVENT_ID,
|
||||
pollKind: PollKind = PollKind.Disclosed,
|
||||
answerItems: List<PollAnswerItem> = listOf(
|
||||
aPollAnswerItem(A_POLL_ANSWER_1),
|
||||
aPollAnswerItem(A_POLL_ANSWER_2),
|
||||
aPollAnswerItem(A_POLL_ANSWER_3),
|
||||
aPollAnswerItem(A_POLL_ANSWER_4),
|
||||
),
|
||||
isEnded: Boolean = false,
|
||||
isMine: Boolean = false,
|
||||
isEditable: Boolean = false,
|
||||
question: String = A_POLL_QUESTION,
|
||||
) = PollContentState(
|
||||
eventId = eventId,
|
||||
question = question,
|
||||
answerItems = answerItems.toImmutableList(),
|
||||
pollKind = pollKind,
|
||||
isPollEditable = isEditable,
|
||||
isPollEnded = isEnded,
|
||||
isMine = isMine,
|
||||
)
|
||||
|
||||
private fun aPollAnswerItem(
|
||||
answer: PollAnswer,
|
||||
isSelected: Boolean = false,
|
||||
isEnabled: Boolean = true,
|
||||
isWinner: Boolean = false,
|
||||
showVotes: Boolean = true,
|
||||
votesCount: Int = 0,
|
||||
percentage: Float = 0f,
|
||||
) = PollAnswerItem(
|
||||
answer = answer,
|
||||
isSelected = isSelected,
|
||||
isEnabled = isEnabled,
|
||||
isWinner = isWinner,
|
||||
showVotes = showVotes,
|
||||
votesCount = votesCount,
|
||||
percentage = percentage,
|
||||
)
|
||||
|
||||
private companion object TestData {
|
||||
private const val A_POLL_QUESTION = "What is your favorite food?"
|
||||
private val A_POLL_ANSWER_1 = PollAnswer("id_1", "Pizza")
|
||||
private val A_POLL_ANSWER_2 = PollAnswer("id_2", "Pasta")
|
||||
private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries")
|
||||
private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger")
|
||||
|
||||
private val MY_USER_WINNING_VOTES = persistentMapOf(
|
||||
A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4),
|
||||
// First item (A_USER_ID) is for my vote
|
||||
// winner
|
||||
A_POLL_ANSWER_2 to persistentListOf(A_USER_ID, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9),
|
||||
A_POLL_ANSWER_3 to persistentListOf(),
|
||||
A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_10),
|
||||
)
|
||||
private val OTHER_WINNING_VOTES = persistentMapOf(
|
||||
// A winner
|
||||
A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5),
|
||||
// First item (A_USER_ID) is for my vote
|
||||
A_POLL_ANSWER_2 to persistentListOf(A_USER_ID, A_USER_ID_6),
|
||||
A_POLL_ANSWER_3 to persistentListOf(),
|
||||
// Other winner
|
||||
A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user