jonnyandrew
10 months ago
committed by
GitHub
8 changed files with 435 additions and 66 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Confirm back navigation when editing a poll only if the poll was changed |
@ -0,0 +1,24 @@
@@ -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.impl |
||||
|
||||
internal object PollConstants { |
||||
const val MIN_ANSWERS = 2 |
||||
const val MAX_ANSWERS = 20 |
||||
const val MAX_ANSWER_LENGTH = 240 |
||||
const val MAX_SELECTIONS = 1 |
||||
} |
@ -0,0 +1,130 @@
@@ -0,0 +1,130 @@
|
||||
/* |
||||
* 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 |
||||
|
||||
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.toPersistentList |
||||
|
||||
/** |
||||
* 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) { "" }.toPersistentList(), |
||||
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 + "").toPersistentList()) |
||||
} |
||||
|
||||
/** |
||||
* 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 }.toPersistentList()) |
||||
} |
||||
|
||||
/** |
||||
* 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) |
||||
}.toPersistentList()) |
||||
|
||||
/** |
||||
* 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 }.toPersistentList(), |
||||
isDisclosed = saved["isDisclosed"] as Boolean, |
||||
) |
||||
} |
||||
) |
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
/* |
||||
* 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 |
||||
|
||||
import androidx.compose.runtime.saveable.SaverScope |
||||
import com.google.common.truth.Truth.assertThat |
||||
import kotlinx.collections.immutable.toPersistentList |
||||
import org.junit.Test |
||||
|
||||
class PollFormStateSaverTest { |
||||
companion object { |
||||
val CanSaveScope = SaverScope { true } |
||||
} |
||||
@Test |
||||
fun `test save and restore`() { |
||||
val state = PollFormState( |
||||
question = "question", |
||||
answers = listOf("answer1", "answer2").toPersistentList(), |
||||
isDisclosed = true, |
||||
) |
||||
|
||||
val saved = with(CanSaveScope) { |
||||
with(pollFormStateSaver) { |
||||
save(state) |
||||
} |
||||
} |
||||
|
||||
val restored = saved?.let { |
||||
pollFormStateSaver.restore(it) |
||||
} |
||||
|
||||
assertThat(restored).isEqualTo(state) |
||||
} |
||||
} |
@ -0,0 +1,147 @@
@@ -0,0 +1,147 @@
|
||||
/* |
||||
* 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 |
||||
|
||||
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.toPersistentList |
||||
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").toPersistentList() |
||||
)) |
||||
} |
||||
|
||||
@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).toPersistentList() |
||||
)) |
||||
} |
||||
|
||||
@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("").toPersistentList()) |
||||
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").toPersistentList(), |
||||
isDisclosed = true, |
||||
) |
||||
} |
||||
|
||||
private fun PollFormState.withBlankAnswers(numAnswers: Int): PollFormState = |
||||
copy(answers = List(numAnswers) { "" }.toPersistentList()) |
Loading…
Reference in new issue