Allow polls to be edited (#1869)
Polls can be edited if they do not have any votes --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
@@ -19,6 +19,8 @@ package io.element.android.features.poll.api
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
fun aPollQuestion() = "What type of food should we have at the party?"
|
||||
|
||||
fun aPollAnswerItemList(
|
||||
hasVotes: Boolean = true,
|
||||
isEnded: Boolean = false,
|
||||
@@ -30,7 +32,7 @@ fun aPollAnswerItemList(
|
||||
isEnabled = !isEnded,
|
||||
isWinner = isEnded,
|
||||
votesCount = if (hasVotes) 5 else 0,
|
||||
percentage = 0.5f
|
||||
percentage = if (hasVotes) 0.5f else 0f
|
||||
),
|
||||
aPollAnswerItem(
|
||||
answer = PollAnswer("option_2", "Chinese \uD83C\uDDE8\uD83C\uDDF3"),
|
||||
@@ -47,9 +49,14 @@ fun aPollAnswerItemList(
|
||||
isWinner = false,
|
||||
isSelected = true,
|
||||
votesCount = if (hasVotes) 1 else 0,
|
||||
percentage = 0.1f
|
||||
percentage = if (hasVotes) 0.1f else 0f
|
||||
),
|
||||
aPollAnswerItem(
|
||||
isDisclosed = isDisclosed,
|
||||
isEnabled = !isEnded,
|
||||
votesCount = if (hasVotes) 4 else 0,
|
||||
percentage = if (hasVotes) 0.4f else 0f,
|
||||
),
|
||||
aPollAnswerItem(isDisclosed = isDisclosed, isEnabled = !isEnded),
|
||||
)
|
||||
|
||||
fun aPollAnswerItem(
|
||||
|
||||
@@ -55,6 +55,7 @@ fun PollContentView(
|
||||
question: String,
|
||||
answerItems: ImmutableList<PollAnswerItem>,
|
||||
pollKind: PollKind,
|
||||
isPollEditable: Boolean,
|
||||
isPollEnded: Boolean,
|
||||
isMine: Boolean,
|
||||
onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit,
|
||||
@@ -103,8 +104,8 @@ fun PollContentView(
|
||||
|
||||
if (isMine) {
|
||||
CreatorView(
|
||||
votesCount = 1, // TODO Polls: set to `votesCount` when edit poll screen is implemented.
|
||||
isPollEnded = isPollEnded,
|
||||
isPollEditable = isPollEditable,
|
||||
onPollEdit = ::onPollEdit,
|
||||
onPollEnd = { showConfirmation = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -197,26 +198,25 @@ private fun ColumnScope.UndisclosedPollBottomNotice(
|
||||
|
||||
@Composable
|
||||
private fun CreatorView(
|
||||
@Suppress("SameParameterValue") votesCount: Int, // TODO Polls: remove @Suppress when edit poll screen is implemented.
|
||||
isPollEnded: Boolean,
|
||||
isPollEditable: Boolean,
|
||||
onPollEdit: () -> Unit,
|
||||
onPollEnd: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (!isPollEnded) {
|
||||
if (votesCount == 0) {
|
||||
when {
|
||||
isPollEditable ->
|
||||
Button(
|
||||
text = stringResource(id = CommonStrings.action_edit_poll),
|
||||
onClick = onPollEdit,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
!isPollEnded ->
|
||||
Button(
|
||||
text = stringResource(id = CommonStrings.action_end_poll),
|
||||
onClick = onPollEnd,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ internal fun PollContentUndisclosedPreview() = ElementPreview {
|
||||
answerItems = aPollAnswerItemList(isDisclosed = false),
|
||||
pollKind = PollKind.Undisclosed,
|
||||
isPollEnded = false,
|
||||
isPollEditable = false,
|
||||
isMine = false,
|
||||
onAnswerSelected = { _, _ -> },
|
||||
onPollEdit = {},
|
||||
@@ -245,6 +246,7 @@ internal fun PollContentDisclosedPreview() = ElementPreview {
|
||||
answerItems = aPollAnswerItemList(),
|
||||
pollKind = PollKind.Disclosed,
|
||||
isPollEnded = false,
|
||||
isPollEditable = false,
|
||||
isMine = false,
|
||||
onAnswerSelected = { _, _ -> },
|
||||
onPollEdit = {},
|
||||
@@ -261,6 +263,7 @@ internal fun PollContentEndedPreview() = ElementPreview {
|
||||
answerItems = aPollAnswerItemList(isEnded = true),
|
||||
pollKind = PollKind.Disclosed,
|
||||
isPollEnded = true,
|
||||
isPollEditable = false,
|
||||
isMine = false,
|
||||
onAnswerSelected = { _, _ -> },
|
||||
onPollEdit = {},
|
||||
@@ -270,13 +273,14 @@ internal fun PollContentEndedPreview() = ElementPreview {
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun PollContentCreatorNoVotesPreview() = ElementPreview {
|
||||
internal fun PollContentCreatorEditablePreview() = ElementPreview {
|
||||
PollContentView(
|
||||
eventId = EventId("\$anEventId"),
|
||||
question = "What type of food should we have at the party?",
|
||||
answerItems = aPollAnswerItemList(hasVotes = false, isEnded = false),
|
||||
pollKind = PollKind.Disclosed,
|
||||
isPollEnded = false,
|
||||
isPollEditable = true,
|
||||
isMine = true,
|
||||
onAnswerSelected = { _, _ -> },
|
||||
onPollEdit = {},
|
||||
@@ -293,6 +297,7 @@ internal fun PollContentCreatorPreview() = ElementPreview {
|
||||
answerItems = aPollAnswerItemList(isEnded = false),
|
||||
pollKind = PollKind.Disclosed,
|
||||
isPollEnded = false,
|
||||
isPollEditable = false,
|
||||
isMine = true,
|
||||
onAnswerSelected = { _, _ -> },
|
||||
onPollEdit = {},
|
||||
@@ -309,6 +314,7 @@ internal fun PollContentCreatorEndedPreview() = ElementPreview {
|
||||
answerItems = aPollAnswerItemList(isEnded = true),
|
||||
pollKind = PollKind.Disclosed,
|
||||
isPollEnded = true,
|
||||
isPollEditable = false,
|
||||
isMine = true,
|
||||
onAnswerSelected = { _, _ -> },
|
||||
onPollEdit = {},
|
||||
|
||||
@@ -21,5 +21,14 @@ import com.bumble.appyx.core.node.Node
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
|
||||
interface CreatePollEntryPoint : FeatureEntryPoint {
|
||||
fun createNode(parentNode: Node, buildContext: BuildContext): Node
|
||||
data class Params(
|
||||
val mode: CreatePollMode,
|
||||
)
|
||||
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun params(params: Params): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.poll.api.create
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
sealed interface CreatePollMode {
|
||||
data object NewPoll : CreatePollMode
|
||||
data class EditPoll(val eventId: EventId) : CreatePollMode
|
||||
}
|
||||
@@ -19,7 +19,7 @@ package io.element.android.features.poll.impl.create
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
|
||||
sealed interface CreatePollEvents {
|
||||
data object Create : CreatePollEvents
|
||||
data object Save : CreatePollEvents
|
||||
data class SetQuestion(val question: String) : CreatePollEvents
|
||||
data class SetAnswer(val index: Int, val text: String) : CreatePollEvents
|
||||
data object AddAnswer : CreatePollEvents
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -26,6 +26,9 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.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.services.analytics.api.AnalyticsService
|
||||
|
||||
@@ -37,7 +40,11 @@ class CreatePollNode @AssistedInject constructor(
|
||||
analyticsService: AnalyticsService,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
private val presenter = presenterFactory.create(backNavigator = ::navigateUp)
|
||||
data class Inputs(val mode: CreatePollMode) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
|
||||
private val presenter = presenterFactory.create(backNavigator = ::navigateUp, mode = inputs.mode)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.features.poll.impl.create
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -32,9 +33,11 @@ import dagger.assisted.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.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.room.MatrixRoom
|
||||
import io.element.android.services.analytics.api.AnalyticsService
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -47,26 +50,39 @@ private const val MAX_ANSWER_LENGTH = 240
|
||||
private const val MAX_SELECTIONS = 1
|
||||
|
||||
class CreatePollPresenter @AssistedInject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val repository: PollRepository,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val messageComposerContext: MessageComposerContext,
|
||||
@Assisted private val navigateUp: () -> Unit,
|
||||
@Assisted private val mode: CreatePollMode,
|
||||
) : Presenter<CreatePollState> {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(backNavigator: () -> Unit): CreatePollPresenter
|
||||
fun create(backNavigator: () -> Unit, mode: CreatePollMode): CreatePollPresenter
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): CreatePollState {
|
||||
|
||||
var question: String by rememberSaveable { mutableStateOf("") }
|
||||
var answers: List<String> by rememberSaveable() { mutableStateOf(listOf("", "")) }
|
||||
var answers: List<String> by rememberSaveable { mutableStateOf(listOf("", "")) }
|
||||
var pollKind: PollKind by rememberSaveable(saver = pollKindSaver) { mutableStateOf(PollKind.Disclosed) }
|
||||
var showConfirmation: Boolean by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val canCreate: Boolean by remember { derivedStateOf { canCreate(question, answers) } }
|
||||
LaunchedEffect(Unit) {
|
||||
if (mode is CreatePollMode.EditPoll) {
|
||||
repository.getPoll(mode.eventId).onSuccess {
|
||||
question = it.question
|
||||
answers = it.answers.map(PollAnswer::text)
|
||||
pollKind = it.kind
|
||||
}.onFailure {
|
||||
analyticsService.trackGetPollFailed(it)
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val canSave: Boolean by remember { derivedStateOf { canSave(question, answers) } }
|
||||
val canAddAnswer: Boolean by remember { derivedStateOf { canAddAnswer(answers) } }
|
||||
val immutableAnswers: ImmutableList<Answer> by remember { derivedStateOf { answers.toAnswers() } }
|
||||
|
||||
@@ -74,29 +90,25 @@ class CreatePollPresenter @AssistedInject constructor(
|
||||
|
||||
fun handleEvents(event: CreatePollEvents) {
|
||||
when (event) {
|
||||
is CreatePollEvents.Create -> scope.launch {
|
||||
if (canCreate) {
|
||||
room.createPoll(
|
||||
is CreatePollEvents.Save -> scope.launch {
|
||||
if (canSave) {
|
||||
repository.savePoll(
|
||||
existingPollId = when (mode) {
|
||||
is CreatePollMode.EditPoll -> mode.eventId
|
||||
is CreatePollMode.NewPoll -> null
|
||||
},
|
||||
question = question,
|
||||
answers = answers,
|
||||
maxSelections = MAX_SELECTIONS,
|
||||
pollKind = pollKind,
|
||||
)
|
||||
analyticsService.capture(
|
||||
Composer(
|
||||
inThread = messageComposerContext.composerMode.inThread,
|
||||
isEditing = messageComposerContext.composerMode.isEditing,
|
||||
isReply = messageComposerContext.composerMode.isReply,
|
||||
messageType = Composer.MessageType.Poll,
|
||||
)
|
||||
)
|
||||
analyticsService.capture(
|
||||
PollCreation(
|
||||
action = PollCreation.Action.Create,
|
||||
maxSelections = MAX_SELECTIONS,
|
||||
).onSuccess {
|
||||
analyticsService.capturePollSaved(
|
||||
isUndisclosed = pollKind == PollKind.Undisclosed,
|
||||
numberOfAnswers = answers.size,
|
||||
)
|
||||
)
|
||||
}.onFailure {
|
||||
analyticsService.trackSavePollFailed(it, mode)
|
||||
}
|
||||
navigateUp()
|
||||
} else {
|
||||
Timber.d("Cannot create poll")
|
||||
@@ -135,7 +147,11 @@ class CreatePollPresenter @AssistedInject constructor(
|
||||
}
|
||||
|
||||
return CreatePollState(
|
||||
canCreate = canCreate,
|
||||
mode = when (mode) {
|
||||
is CreatePollMode.NewPoll -> CreatePollState.Mode.New
|
||||
is CreatePollMode.EditPoll -> CreatePollState.Mode.Edit
|
||||
},
|
||||
canSave = canSave,
|
||||
canAddAnswer = canAddAnswer,
|
||||
question = question,
|
||||
answers = immutableAnswers,
|
||||
@@ -144,16 +160,61 @@ class CreatePollPresenter @AssistedInject constructor(
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
||||
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 canCreate(
|
||||
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)
|
||||
}
|
||||
|
||||
private fun canSave(
|
||||
question: String,
|
||||
answers: List<String>
|
||||
) = question.isNotBlank() && answers.size >= MIN_ANSWERS && answers.all { it.isNotBlank() }
|
||||
|
||||
private fun canAddAnswer(answers: List<String>) = answers.size < MAX_ANSWERS
|
||||
|
||||
private fun List<String>.toAnswers(): ImmutableList<Answer> {
|
||||
fun List<String>.toAnswers(): ImmutableList<Answer> {
|
||||
return map { answer ->
|
||||
Answer(
|
||||
text = answer,
|
||||
|
||||
@@ -20,14 +20,20 @@ import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class CreatePollState(
|
||||
val canCreate: Boolean,
|
||||
val mode: Mode,
|
||||
val canSave: Boolean,
|
||||
val canAddAnswer: Boolean,
|
||||
val question: String,
|
||||
val answers: ImmutableList<Answer>,
|
||||
val pollKind: PollKind,
|
||||
val showConfirmation: Boolean,
|
||||
val eventSink: (CreatePollEvents) -> Unit,
|
||||
)
|
||||
) {
|
||||
enum class Mode {
|
||||
New,
|
||||
Edit,
|
||||
}
|
||||
}
|
||||
|
||||
data class Answer(
|
||||
val text: String,
|
||||
|
||||
@@ -25,6 +25,7 @@ class CreatePollStateProvider : PreviewParameterProvider<CreatePollState> {
|
||||
override val values: Sequence<CreatePollState>
|
||||
get() = sequenceOf(
|
||||
aCreatePollState(
|
||||
mode = CreatePollState.Mode.New,
|
||||
canCreate = false,
|
||||
canAddAnswer = true,
|
||||
question = "",
|
||||
@@ -36,6 +37,7 @@ class CreatePollStateProvider : PreviewParameterProvider<CreatePollState> {
|
||||
showConfirmation = false,
|
||||
),
|
||||
aCreatePollState(
|
||||
mode = CreatePollState.Mode.New,
|
||||
canCreate = true,
|
||||
canAddAnswer = true,
|
||||
question = "What type of food should we have?",
|
||||
@@ -47,6 +49,7 @@ class CreatePollStateProvider : PreviewParameterProvider<CreatePollState> {
|
||||
pollKind = PollKind.Undisclosed,
|
||||
),
|
||||
aCreatePollState(
|
||||
mode = CreatePollState.Mode.New,
|
||||
canCreate = true,
|
||||
canAddAnswer = true,
|
||||
question = "What type of food should we have?",
|
||||
@@ -58,6 +61,7 @@ class CreatePollStateProvider : PreviewParameterProvider<CreatePollState> {
|
||||
pollKind = PollKind.Undisclosed,
|
||||
),
|
||||
aCreatePollState(
|
||||
mode = CreatePollState.Mode.New,
|
||||
canCreate = true,
|
||||
canAddAnswer = true,
|
||||
question = "What type of food should we have?",
|
||||
@@ -71,6 +75,7 @@ class CreatePollStateProvider : PreviewParameterProvider<CreatePollState> {
|
||||
pollKind = PollKind.Undisclosed,
|
||||
),
|
||||
aCreatePollState(
|
||||
mode = CreatePollState.Mode.New,
|
||||
canCreate = true,
|
||||
canAddAnswer = false,
|
||||
question = "Should there be more than 20 answers?",
|
||||
@@ -100,6 +105,7 @@ class CreatePollStateProvider : PreviewParameterProvider<CreatePollState> {
|
||||
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." +
|
||||
@@ -120,11 +126,24 @@ class CreatePollStateProvider : PreviewParameterProvider<CreatePollState> {
|
||||
),
|
||||
showConfirmation = false,
|
||||
pollKind = PollKind.Undisclosed,
|
||||
)
|
||||
),
|
||||
aCreatePollState(
|
||||
mode = CreatePollState.Mode.Edit,
|
||||
canCreate = false,
|
||||
canAddAnswer = true,
|
||||
question = "",
|
||||
answers = persistentListOf(
|
||||
Answer("", false),
|
||||
Answer("", false)
|
||||
),
|
||||
pollKind = PollKind.Disclosed,
|
||||
showConfirmation = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun aCreatePollState(
|
||||
mode: CreatePollState.Mode,
|
||||
canCreate: Boolean,
|
||||
canAddAnswer: Boolean,
|
||||
question: String,
|
||||
@@ -133,7 +152,8 @@ private fun aCreatePollState(
|
||||
pollKind: PollKind
|
||||
): CreatePollState {
|
||||
return CreatePollState(
|
||||
canCreate = canCreate,
|
||||
mode = mode,
|
||||
canSave = canCreate,
|
||||
canAddAnswer = canAddAnswer,
|
||||
question = question,
|
||||
answers = answers,
|
||||
|
||||
@@ -47,8 +47,8 @@ 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.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
@@ -67,7 +67,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CreatePollView(
|
||||
state: CreatePollState,
|
||||
@@ -90,23 +89,11 @@ fun CreatePollView(
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.screen_create_poll_title),
|
||||
style = ElementTheme.typography.aliasScreenTitle,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
BackButton(onClick = navBack)
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
text = stringResource(id = CommonStrings.action_create),
|
||||
onClick = { state.eventSink(CreatePollEvents.Create) },
|
||||
enabled = state.canCreate,
|
||||
)
|
||||
}
|
||||
CreatePollTopAppBar(
|
||||
mode = state.mode,
|
||||
saveEnabled = state.canSave,
|
||||
onBackPress = navBack,
|
||||
onSaveClicked = { state.eventSink(CreatePollEvents.Save) }
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
@@ -210,6 +197,40 @@ fun CreatePollView(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CreatePollTopAppBar(
|
||||
mode: CreatePollState.Mode,
|
||||
saveEnabled: Boolean,
|
||||
onBackPress: () -> Unit = {},
|
||||
onSaveClicked: () -> Unit = {},
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = when (mode) {
|
||||
CreatePollState.Mode.New -> stringResource(id = R.string.screen_create_poll_title)
|
||||
CreatePollState.Mode.Edit -> stringResource(id = R.string.screen_edit_poll_title)
|
||||
},
|
||||
style = ElementTheme.typography.aliasScreenTitle,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackPress)
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
text = when (mode) {
|
||||
CreatePollState.Mode.New -> stringResource(id = CommonStrings.action_create)
|
||||
CreatePollState.Mode.Edit -> stringResource(id = CommonStrings.action_done)
|
||||
},
|
||||
onClick = onSaveClicked,
|
||||
enabled = saveEnabled,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun CreatePollViewPreview(
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.element.android.features.poll.impl.create
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.poll.api.create.CreatePollEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
@@ -26,7 +27,19 @@ import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultCreatePollEntryPoint @Inject constructor() : CreatePollEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<CreatePollNode>(buildContext)
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreatePollEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : CreatePollEntryPoint.NodeBuilder {
|
||||
|
||||
override fun params(params: CreatePollEntryPoint.Params): CreatePollEntryPoint.NodeBuilder {
|
||||
plugins += CreatePollNode.Inputs(mode = params.mode)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<CreatePollNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.poll.impl.data
|
||||
|
||||
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.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
class PollRepository @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
) {
|
||||
suspend fun getPoll(eventId: EventId): Result<PollContent> = runCatching {
|
||||
room.timeline
|
||||
.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 -> room.createPoll(
|
||||
question = question,
|
||||
answers = answers,
|
||||
maxSelections = maxSelections,
|
||||
pollKind = pollKind,
|
||||
)
|
||||
else -> room.editPoll(
|
||||
pollStartId = existingPollId,
|
||||
question = question,
|
||||
answers = answers,
|
||||
maxSelections = maxSelections,
|
||||
pollKind = pollKind,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@
|
||||
<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_title">"Edit poll"</string>
|
||||
</resources>
|
||||
|
||||
@@ -18,14 +18,25 @@ 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
|
||||
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.data.PollRepository
|
||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||
import io.element.android.libraries.matrix.test.room.CreatePollInvocation
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
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.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.room.SavePollInvocation
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aPollContent
|
||||
import io.element.android.libraries.matrix.test.room.anEventTimelineItem
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -38,58 +49,73 @@ class CreatePollPresenterTest {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
private val pollEventId = AN_EVENT_ID
|
||||
private var navUpInvocationsCount = 0
|
||||
private val fakeMatrixRoom = FakeMatrixRoom()
|
||||
private val existingPoll = anExistingPoll()
|
||||
private val fakeMatrixRoom = createFakeMatrixRoom(existingPoll)
|
||||
private val fakeAnalyticsService = FakeAnalyticsService()
|
||||
private val fakeMessageComposerContext = FakeMessageComposerContext()
|
||||
|
||||
private val presenter = CreatePollPresenter(
|
||||
room = fakeMatrixRoom,
|
||||
analyticsService = fakeAnalyticsService,
|
||||
messageComposerContext = fakeMessageComposerContext,
|
||||
navigateUp = { navUpInvocationsCount++ },
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `default state has proper default values`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().let {
|
||||
Truth.assertThat(it.canCreate).isEqualTo(false)
|
||||
Truth.assertThat(it.canAddAnswer).isEqualTo(true)
|
||||
Truth.assertThat(it.question).isEqualTo("")
|
||||
Truth.assertThat(it.answers).isEqualTo(listOf(Answer("", false), Answer("", false)))
|
||||
Truth.assertThat(it.pollKind).isEqualTo(PollKind.Disclosed)
|
||||
Truth.assertThat(it.showConfirmation).isEqualTo(false)
|
||||
}
|
||||
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 = createFakeMatrixRoom(existingPoll = null)
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
Truth.assertThat(fakeAnalyticsService.trackedErrors.filterIsInstance<CreatePollException.GetPollFailed>()).isNotEmpty()
|
||||
Truth.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()
|
||||
Truth.assertThat(initial.canCreate).isEqualTo(false)
|
||||
Truth.assertThat(initial.canSave).isFalse()
|
||||
|
||||
initial.eventSink(CreatePollEvents.SetQuestion("A question?"))
|
||||
val questionSet = awaitItem()
|
||||
Truth.assertThat(questionSet.canCreate).isEqualTo(false)
|
||||
Truth.assertThat(questionSet.canSave).isFalse()
|
||||
|
||||
questionSet.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1"))
|
||||
val answer1Set = awaitItem()
|
||||
Truth.assertThat(answer1Set.canCreate).isEqualTo(false)
|
||||
Truth.assertThat(answer1Set.canSave).isFalse()
|
||||
|
||||
answer1Set.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2"))
|
||||
val answer2Set = awaitItem()
|
||||
Truth.assertThat(answer2Set.canCreate).isEqualTo(true)
|
||||
Truth.assertThat(answer2Set.canSave).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create polls sends a poll start event`() = runTest {
|
||||
fun `create poll sends a poll start event`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -98,11 +124,11 @@ class CreatePollPresenterTest {
|
||||
initial.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1"))
|
||||
initial.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2"))
|
||||
skipItems(3)
|
||||
initial.eventSink(CreatePollEvents.Create)
|
||||
initial.eventSink(CreatePollEvents.Save)
|
||||
delay(1) // Wait for the coroutine to finish
|
||||
Truth.assertThat(fakeMatrixRoom.createPollInvocations.size).isEqualTo(1)
|
||||
Truth.assertThat(fakeMatrixRoom.createPollInvocations.last()).isEqualTo(
|
||||
CreatePollInvocation(
|
||||
SavePollInvocation(
|
||||
question = "A question?",
|
||||
answers = listOf("Answer 1", "Answer 2"),
|
||||
maxSelections = 1,
|
||||
@@ -128,8 +154,104 @@ class CreatePollPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when poll creation fails, error is tracked`() = runTest {
|
||||
val error = Exception("cause")
|
||||
fakeMatrixRoom.givenCreatePollResult(Result.failure(error))
|
||||
val presenter = createCreatePollPresenter(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
|
||||
Truth.assertThat(fakeMatrixRoom.createPollInvocations).hasSize(1)
|
||||
Truth.assertThat(fakeAnalyticsService.capturedEvents).isEmpty()
|
||||
Truth.assertThat(fakeAnalyticsService.trackedErrors).hasSize(1)
|
||||
Truth.assertThat(fakeAnalyticsService.trackedErrors).containsExactly(
|
||||
CreatePollException.SavePollFailed("Failed to create poll", error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `edit poll sends a poll edit event`() = runTest {
|
||||
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)
|
||||
}
|
||||
delay(1) // Wait for the coroutine to finish
|
||||
Truth.assertThat(fakeMatrixRoom.editPollInvocations.size).isEqualTo(1)
|
||||
Truth.assertThat(fakeMatrixRoom.editPollInvocations.last()).isEqualTo(
|
||||
SavePollInvocation(
|
||||
question = "Changed question",
|
||||
answers = listOf("Changed answer 1", "Changed answer 2", "Maybe"),
|
||||
maxSelections = 1,
|
||||
pollKind = PollKind.Disclosed
|
||||
)
|
||||
)
|
||||
Truth.assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2)
|
||||
Truth.assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo(
|
||||
Composer(
|
||||
inThread = false,
|
||||
isEditing = true,
|
||||
isReply = false,
|
||||
messageType = Composer.MessageType.Poll,
|
||||
)
|
||||
)
|
||||
Truth.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")
|
||||
fakeMatrixRoom.givenEditPollResult(Result.failure(error))
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded().eventSink(CreatePollEvents.SetAnswer(0, "A"))
|
||||
awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvents.Save)
|
||||
delay(1) // Wait for the coroutine to finish
|
||||
Truth.assertThat(fakeMatrixRoom.editPollInvocations).hasSize(1)
|
||||
Truth.assertThat(fakeAnalyticsService.capturedEvents).isEmpty()
|
||||
Truth.assertThat(fakeAnalyticsService.trackedErrors).hasSize(1)
|
||||
Truth.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 {
|
||||
@@ -139,7 +261,7 @@ class CreatePollPresenterTest {
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
val answerAdded = awaitItem()
|
||||
Truth.assertThat(answerAdded.answers.size).isEqualTo(3)
|
||||
Truth.assertThat(answerAdded.answers[2].text).isEqualTo("")
|
||||
Truth.assertThat(answerAdded.answers[2].text).isEmpty()
|
||||
|
||||
initial.eventSink(CreatePollEvents.RemoveAnswer(2))
|
||||
val answerRemoved = awaitItem()
|
||||
@@ -149,6 +271,7 @@ class CreatePollPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `set question sets the question`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -161,6 +284,7 @@ class CreatePollPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `set poll answer sets the given poll answer`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -173,6 +297,7 @@ class CreatePollPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `set poll kind sets the poll kind`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -185,34 +310,37 @@ class CreatePollPresenterTest {
|
||||
|
||||
@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()
|
||||
Truth.assertThat(initial.canAddAnswer).isEqualTo(true)
|
||||
Truth.assertThat(initial.canAddAnswer).isTrue()
|
||||
repeat(17) {
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
Truth.assertThat(awaitItem().canAddAnswer).isEqualTo(true)
|
||||
Truth.assertThat(awaitItem().canAddAnswer).isTrue()
|
||||
}
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
Truth.assertThat(awaitItem().canAddAnswer).isEqualTo(false)
|
||||
Truth.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()
|
||||
Truth.assertThat(initial.answers.all { it.canDelete }).isEqualTo(false)
|
||||
Truth.assertThat(initial.answers.all { it.canDelete }).isFalse()
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
Truth.assertThat(awaitItem().answers.all { it.canDelete }).isEqualTo(true)
|
||||
Truth.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 {
|
||||
@@ -224,6 +352,7 @@ class CreatePollPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `navBack event calls navBack lambda`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -236,31 +365,104 @@ class CreatePollPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `confirm nav back with blank fields calls nav back lambda`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
Truth.assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
Truth.assertThat(initial.showConfirmation).isEqualTo(false)
|
||||
Truth.assertThat(initial.showConfirmation).isFalse()
|
||||
initial.eventSink(CreatePollEvents.ConfirmNavBack)
|
||||
Truth.assertThat(navUpInvocationsCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirm nav back with non blank fields shows confirmation dialog and sending hide hids it`() = runTest {
|
||||
fun `confirm nav back with non blank fields shows confirmation dialog and sending hides it`() = runTest {
|
||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.NewPoll)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetQuestion("Non blank"))
|
||||
Truth.assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
Truth.assertThat(awaitItem().showConfirmation).isEqualTo(false)
|
||||
Truth.assertThat(awaitItem().showConfirmation).isFalse()
|
||||
initial.eventSink(CreatePollEvents.ConfirmNavBack)
|
||||
Truth.assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
Truth.assertThat(awaitItem().showConfirmation).isEqualTo(true)
|
||||
Truth.assertThat(awaitItem().showConfirmation).isTrue()
|
||||
initial.eventSink(CreatePollEvents.HideConfirmation)
|
||||
Truth.assertThat(awaitItem().showConfirmation).isEqualTo(false)
|
||||
Truth.assertThat(awaitItem().showConfirmation).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TurbineTestContext<CreatePollState>.awaitDefaultItem() =
|
||||
awaitItem().apply {
|
||||
Truth.assertThat(canSave).isFalse()
|
||||
Truth.assertThat(canAddAnswer).isTrue()
|
||||
Truth.assertThat(question).isEmpty()
|
||||
Truth.assertThat(answers).isEqualTo(listOf(Answer("", false), Answer("", false)))
|
||||
Truth.assertThat(pollKind).isEqualTo(PollKind.Disclosed)
|
||||
Truth.assertThat(showConfirmation).isFalse()
|
||||
}
|
||||
|
||||
private suspend fun TurbineTestContext<CreatePollState>.awaitPollLoaded(
|
||||
newQuestion: String? = null,
|
||||
newAnswer1: String? = null,
|
||||
newAnswer2: String? = null,
|
||||
) =
|
||||
awaitItem().apply {
|
||||
Truth.assertThat(canSave).isTrue()
|
||||
Truth.assertThat(canAddAnswer).isTrue()
|
||||
Truth.assertThat(question).isEqualTo(newQuestion ?: existingPoll.question)
|
||||
Truth.assertThat(answers).isEqualTo(existingPoll.expectedAnswersState().toMutableList().apply {
|
||||
newAnswer1?.let { this[0] = Answer(it, true) }
|
||||
newAnswer2?.let { this[1] = Answer(it, true) }
|
||||
})
|
||||
Truth.assertThat(pollKind).isEqualTo(existingPoll.kind)
|
||||
}
|
||||
|
||||
private fun createCreatePollPresenter(
|
||||
mode: CreatePollMode = CreatePollMode.NewPoll,
|
||||
room: MatrixRoom = fakeMatrixRoom,
|
||||
): CreatePollPresenter = CreatePollPresenter(
|
||||
repository = PollRepository(room),
|
||||
analyticsService = fakeAnalyticsService,
|
||||
messageComposerContext = fakeMessageComposerContext,
|
||||
navigateUp = { navUpInvocationsCount++ },
|
||||
mode = mode,
|
||||
)
|
||||
|
||||
private fun createFakeMatrixRoom(
|
||||
existingPoll: PollContent? = anExistingPoll(),
|
||||
) = FakeMatrixRoom(
|
||||
matrixTimeline = FakeMatrixTimeline(
|
||||
initialTimelineItems = existingPoll?.let {
|
||||
listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
0,
|
||||
anEventTimelineItem(
|
||||
eventId = pollEventId,
|
||||
content = it,
|
||||
)
|
||||
)
|
||||
)
|
||||
}.orEmpty()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun anExistingPoll() = aPollContent(
|
||||
question = "Do you like polls?",
|
||||
answers = listOf(
|
||||
PollAnswer("1", "Yes"),
|
||||
PollAnswer("2", "No"),
|
||||
PollAnswer("2", "Maybe"),
|
||||
),
|
||||
)
|
||||
|
||||
private fun PollContent.expectedAnswersState() = answers.map { answer ->
|
||||
Answer(
|
||||
text = answer.text,
|
||||
canDelete = answers.size > 2,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user