Merge pull request #5845 from element-hq/feature/bma/unsavedChangeDialog
Update unsaved change dialog
This commit is contained in:
@@ -10,15 +10,15 @@ 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
|
||||
sealed interface CreatePollEvent {
|
||||
data object Save : CreatePollEvent
|
||||
data class Delete(val confirmed: Boolean) : CreatePollEvent
|
||||
data class SetQuestion(val question: String) : CreatePollEvent
|
||||
data class SetAnswer(val index: Int, val text: String) : CreatePollEvent
|
||||
data object AddAnswer : CreatePollEvent
|
||||
data class RemoveAnswer(val index: Int) : CreatePollEvent
|
||||
data class SetPollKind(val pollKind: PollKind) : CreatePollEvent
|
||||
data object NavBack : CreatePollEvent
|
||||
data object ConfirmNavBack : CreatePollEvent
|
||||
data object HideConfirmation : CreatePollEvent
|
||||
}
|
||||
@@ -97,9 +97,9 @@ class CreatePollPresenter(
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
fun handleEvent(event: CreatePollEvents) {
|
||||
fun handleEvent(event: CreatePollEvent) {
|
||||
when (event) {
|
||||
is CreatePollEvents.Save -> scope.launch {
|
||||
is CreatePollEvent.Save -> scope.launch {
|
||||
if (canSave) {
|
||||
repository.savePoll(
|
||||
existingPollId = when (mode) {
|
||||
@@ -123,7 +123,7 @@ class CreatePollPresenter(
|
||||
Timber.d("Cannot create poll")
|
||||
}
|
||||
}
|
||||
is CreatePollEvents.Delete -> {
|
||||
is CreatePollEvent.Delete -> {
|
||||
if (mode !is CreatePollMode.EditPoll) {
|
||||
return
|
||||
}
|
||||
@@ -139,25 +139,25 @@ class CreatePollPresenter(
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
is CreatePollEvents.AddAnswer -> {
|
||||
is CreatePollEvent.AddAnswer -> {
|
||||
poll = poll.withNewAnswer()
|
||||
}
|
||||
is CreatePollEvents.RemoveAnswer -> {
|
||||
is CreatePollEvent.RemoveAnswer -> {
|
||||
poll = poll.withAnswerRemoved(event.index)
|
||||
}
|
||||
is CreatePollEvents.SetAnswer -> {
|
||||
is CreatePollEvent.SetAnswer -> {
|
||||
poll = poll.withAnswerChanged(event.index, event.text)
|
||||
}
|
||||
is CreatePollEvents.SetPollKind -> {
|
||||
is CreatePollEvent.SetPollKind -> {
|
||||
poll = poll.copy(isDisclosed = event.pollKind.isDisclosed)
|
||||
}
|
||||
is CreatePollEvents.SetQuestion -> {
|
||||
is CreatePollEvent.SetQuestion -> {
|
||||
poll = poll.copy(question = event.question)
|
||||
}
|
||||
is CreatePollEvents.NavBack -> {
|
||||
is CreatePollEvent.NavBack -> {
|
||||
navigateUp()
|
||||
}
|
||||
CreatePollEvents.ConfirmNavBack -> {
|
||||
CreatePollEvent.ConfirmNavBack -> {
|
||||
val shouldConfirm = isDirty
|
||||
if (shouldConfirm) {
|
||||
showBackConfirmation = true
|
||||
@@ -165,7 +165,7 @@ class CreatePollPresenter(
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
is CreatePollEvents.HideConfirmation -> {
|
||||
is CreatePollEvent.HideConfirmation -> {
|
||||
showBackConfirmation = false
|
||||
showDeleteConfirmation = false
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ data class CreatePollState(
|
||||
val pollKind: PollKind,
|
||||
val showBackConfirmation: Boolean,
|
||||
val showDeleteConfirmation: Boolean,
|
||||
val eventSink: (CreatePollEvents) -> Unit,
|
||||
val eventSink: (CreatePollEvent) -> Unit,
|
||||
) {
|
||||
enum class Mode {
|
||||
New,
|
||||
|
||||
@@ -62,20 +62,21 @@ fun CreatePollView(
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) }
|
||||
val navBack = { state.eventSink(CreatePollEvent.ConfirmNavBack) }
|
||||
BackHandler(onBack = navBack)
|
||||
if (state.showBackConfirmation) {
|
||||
SaveChangesDialog(
|
||||
onSubmitClick = { state.eventSink(CreatePollEvents.NavBack) },
|
||||
onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) }
|
||||
onSaveClick = { state.eventSink(CreatePollEvent.Save) },
|
||||
onDiscardClick = { state.eventSink(CreatePollEvent.NavBack) },
|
||||
onDismiss = { state.eventSink(CreatePollEvent.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) }
|
||||
onSubmitClick = { state.eventSink(CreatePollEvent.Delete(confirmed = true)) },
|
||||
onDismiss = { state.eventSink(CreatePollEvent.HideConfirmation) }
|
||||
)
|
||||
}
|
||||
val questionFocusRequester = remember { FocusRequester() }
|
||||
@@ -90,7 +91,7 @@ fun CreatePollView(
|
||||
mode = state.mode,
|
||||
saveEnabled = state.canSave,
|
||||
onBackClick = navBack,
|
||||
onSaveClick = { state.eventSink(CreatePollEvents.Save) }
|
||||
onSaveClick = { state.eventSink(CreatePollEvent.Save) }
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
@@ -111,7 +112,7 @@ fun CreatePollView(
|
||||
label = stringResource(id = R.string.screen_create_poll_question_desc),
|
||||
value = state.question,
|
||||
onValueChange = {
|
||||
state.eventSink(CreatePollEvents.SetQuestion(it))
|
||||
state.eventSink(CreatePollEvent.SetQuestion(it))
|
||||
},
|
||||
modifier = Modifier
|
||||
.focusRequester(questionFocusRequester)
|
||||
@@ -130,7 +131,7 @@ fun CreatePollView(
|
||||
TextField(
|
||||
value = answer.text,
|
||||
onValueChange = {
|
||||
state.eventSink(CreatePollEvents.SetAnswer(index, it))
|
||||
state.eventSink(CreatePollEvent.SetAnswer(index, it))
|
||||
},
|
||||
modifier = Modifier
|
||||
.then(if (isLastItem) Modifier.focusRequester(answerFocusRequester) else Modifier)
|
||||
@@ -144,7 +145,7 @@ fun CreatePollView(
|
||||
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))
|
||||
state.eventSink(CreatePollEvent.RemoveAnswer(index))
|
||||
},
|
||||
)
|
||||
},
|
||||
@@ -160,7 +161,7 @@ fun CreatePollView(
|
||||
),
|
||||
style = ListItemStyle.Primary,
|
||||
onClick = {
|
||||
state.eventSink(CreatePollEvents.AddAnswer)
|
||||
state.eventSink(CreatePollEvent.AddAnswer)
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
lazyListState.animateScrollToItem(state.answers.size + 1)
|
||||
answerFocusRequester.requestFocus()
|
||||
@@ -180,7 +181,7 @@ fun CreatePollView(
|
||||
),
|
||||
onClick = {
|
||||
state.eventSink(
|
||||
CreatePollEvents.SetPollKind(
|
||||
CreatePollEvent.SetPollKind(
|
||||
if (state.pollKind == PollKind.Disclosed) PollKind.Undisclosed else PollKind.Disclosed
|
||||
)
|
||||
)
|
||||
@@ -190,7 +191,7 @@ fun CreatePollView(
|
||||
ListItem(
|
||||
headlineContent = { Text(text = stringResource(id = CommonStrings.action_delete_poll)) },
|
||||
style = ListItemStyle.Destructive,
|
||||
onClick = { state.eventSink(CreatePollEvents.Delete(confirmed = false)) },
|
||||
onClick = { state.eventSink(CreatePollEvent.Delete(confirmed = false)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,15 +104,15 @@ class CreatePollPresenterTest {
|
||||
val initial = awaitItem()
|
||||
assertThat(initial.canSave).isFalse()
|
||||
|
||||
initial.eventSink(CreatePollEvents.SetQuestion("A question?"))
|
||||
initial.eventSink(CreatePollEvent.SetQuestion("A question?"))
|
||||
val questionSet = awaitItem()
|
||||
assertThat(questionSet.canSave).isFalse()
|
||||
|
||||
questionSet.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1"))
|
||||
questionSet.eventSink(CreatePollEvent.SetAnswer(0, "Answer 1"))
|
||||
val answer1Set = awaitItem()
|
||||
assertThat(answer1Set.canSave).isFalse()
|
||||
|
||||
answer1Set.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2"))
|
||||
answer1Set.eventSink(CreatePollEvent.SetAnswer(1, "Answer 2"))
|
||||
val answer2Set = awaitItem()
|
||||
assertThat(answer2Set.canSave).isTrue()
|
||||
}
|
||||
@@ -133,11 +133,11 @@ class CreatePollPresenterTest {
|
||||
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"))
|
||||
initial.eventSink(CreatePollEvent.SetQuestion("A question?"))
|
||||
initial.eventSink(CreatePollEvent.SetAnswer(0, "Answer 1"))
|
||||
initial.eventSink(CreatePollEvent.SetAnswer(1, "Answer 2"))
|
||||
skipItems(3)
|
||||
initial.eventSink(CreatePollEvents.Save)
|
||||
initial.eventSink(CreatePollEvent.Save)
|
||||
delay(1) // Wait for the coroutine to finish
|
||||
createPollResult.assertions().isCalledOnce()
|
||||
.with(
|
||||
@@ -182,10 +182,10 @@ class CreatePollPresenterTest {
|
||||
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)
|
||||
awaitDefaultItem().eventSink(CreatePollEvent.SetQuestion("A question?"))
|
||||
awaitItem().eventSink(CreatePollEvent.SetAnswer(0, "Answer 1"))
|
||||
awaitItem().eventSink(CreatePollEvent.SetAnswer(1, "Answer 2"))
|
||||
awaitItem().eventSink(CreatePollEvent.Save)
|
||||
delay(1) // Wait for the coroutine to finish
|
||||
createPollResult.assertions().isCalledOnce()
|
||||
assertThat(fakeAnalyticsService.capturedEvents).isEmpty()
|
||||
@@ -210,20 +210,20 @@ class CreatePollPresenterTest {
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded().apply {
|
||||
eventSink(CreatePollEvents.SetQuestion("Changed question"))
|
||||
eventSink(CreatePollEvent.SetQuestion("Changed question"))
|
||||
}
|
||||
awaitItem().apply {
|
||||
eventSink(CreatePollEvents.SetAnswer(0, "Changed answer 1"))
|
||||
eventSink(CreatePollEvent.SetAnswer(0, "Changed answer 1"))
|
||||
}
|
||||
awaitItem().apply {
|
||||
eventSink(CreatePollEvents.SetAnswer(1, "Changed answer 2"))
|
||||
eventSink(CreatePollEvent.SetAnswer(1, "Changed answer 2"))
|
||||
}
|
||||
awaitPollLoaded(
|
||||
newQuestion = "Changed question",
|
||||
newAnswer1 = "Changed answer 1",
|
||||
newAnswer2 = "Changed answer 2",
|
||||
).apply {
|
||||
eventSink(CreatePollEvents.Save)
|
||||
eventSink(CreatePollEvent.Save)
|
||||
}
|
||||
advanceUntilIdle() // Wait for the coroutine to finish
|
||||
|
||||
@@ -275,8 +275,8 @@ class CreatePollPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded().eventSink(CreatePollEvents.SetAnswer(0, "A"))
|
||||
awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvents.Save)
|
||||
awaitPollLoaded().eventSink(CreatePollEvent.SetAnswer(0, "A"))
|
||||
awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvent.Save)
|
||||
advanceUntilIdle() // Wait for the coroutine to finish
|
||||
editPollLambda.assertions().isCalledOnce()
|
||||
assertThat(fakeAnalyticsService.capturedEvents).isEmpty()
|
||||
@@ -296,12 +296,12 @@ class CreatePollPresenterTest {
|
||||
val initial = awaitItem()
|
||||
assertThat(initial.answers.size).isEqualTo(2)
|
||||
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
initial.eventSink(CreatePollEvent.AddAnswer)
|
||||
val answerAdded = awaitItem()
|
||||
assertThat(answerAdded.answers.size).isEqualTo(3)
|
||||
assertThat(answerAdded.answers[2].text).isEmpty()
|
||||
|
||||
initial.eventSink(CreatePollEvents.RemoveAnswer(2))
|
||||
initial.eventSink(CreatePollEvent.RemoveAnswer(2))
|
||||
val answerRemoved = awaitItem()
|
||||
assertThat(answerRemoved.answers.size).isEqualTo(2)
|
||||
}
|
||||
@@ -314,7 +314,7 @@ class CreatePollPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetQuestion("A question?"))
|
||||
initial.eventSink(CreatePollEvent.SetQuestion("A question?"))
|
||||
val questionSet = awaitItem()
|
||||
assertThat(questionSet.question).isEqualTo("A question?")
|
||||
}
|
||||
@@ -327,7 +327,7 @@ class CreatePollPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetAnswer(0, "This is answer 1"))
|
||||
initial.eventSink(CreatePollEvent.SetAnswer(0, "This is answer 1"))
|
||||
val answerSet = awaitItem()
|
||||
assertThat(answerSet.answers.first().text).isEqualTo("This is answer 1")
|
||||
}
|
||||
@@ -340,7 +340,7 @@ class CreatePollPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetPollKind(PollKind.Undisclosed))
|
||||
initial.eventSink(CreatePollEvent.SetPollKind(PollKind.Undisclosed))
|
||||
val kindSet = awaitItem()
|
||||
assertThat(kindSet.pollKind).isEqualTo(PollKind.Undisclosed)
|
||||
}
|
||||
@@ -355,10 +355,10 @@ class CreatePollPresenterTest {
|
||||
val initial = awaitItem()
|
||||
assertThat(initial.canAddAnswer).isTrue()
|
||||
repeat(17) {
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
initial.eventSink(CreatePollEvent.AddAnswer)
|
||||
assertThat(awaitItem().canAddAnswer).isTrue()
|
||||
}
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
initial.eventSink(CreatePollEvent.AddAnswer)
|
||||
assertThat(awaitItem().canAddAnswer).isFalse()
|
||||
}
|
||||
}
|
||||
@@ -371,7 +371,7 @@ class CreatePollPresenterTest {
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
assertThat(initial.answers.all { it.canDelete }).isFalse()
|
||||
initial.eventSink(CreatePollEvents.AddAnswer)
|
||||
initial.eventSink(CreatePollEvent.AddAnswer)
|
||||
assertThat(awaitItem().answers.all { it.canDelete }).isTrue()
|
||||
}
|
||||
}
|
||||
@@ -383,7 +383,7 @@ class CreatePollPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetAnswer(0, "A".repeat(241)))
|
||||
initial.eventSink(CreatePollEvent.SetAnswer(0, "A".repeat(241)))
|
||||
assertThat(awaitItem().answers.first().text.length).isEqualTo(240)
|
||||
}
|
||||
}
|
||||
@@ -396,7 +396,7 @@ class CreatePollPresenterTest {
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
initial.eventSink(CreatePollEvents.NavBack)
|
||||
initial.eventSink(CreatePollEvent.NavBack)
|
||||
assertThat(navUpInvocationsCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
@@ -410,7 +410,7 @@ class CreatePollPresenterTest {
|
||||
val initial = awaitItem()
|
||||
assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
assertThat(initial.showBackConfirmation).isFalse()
|
||||
initial.eventSink(CreatePollEvents.ConfirmNavBack)
|
||||
initial.eventSink(CreatePollEvent.ConfirmNavBack)
|
||||
assertThat(navUpInvocationsCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
@@ -422,11 +422,11 @@ class CreatePollPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(CreatePollEvents.SetQuestion("Non blank"))
|
||||
initial.eventSink(CreatePollEvent.SetQuestion("Non blank"))
|
||||
assertThat(awaitItem().showBackConfirmation).isFalse()
|
||||
initial.eventSink(CreatePollEvents.ConfirmNavBack)
|
||||
initial.eventSink(CreatePollEvent.ConfirmNavBack)
|
||||
assertThat(awaitItem().showBackConfirmation).isTrue()
|
||||
initial.eventSink(CreatePollEvents.HideConfirmation)
|
||||
initial.eventSink(CreatePollEvent.HideConfirmation)
|
||||
assertThat(awaitItem().showBackConfirmation).isFalse()
|
||||
assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
}
|
||||
@@ -442,7 +442,7 @@ class CreatePollPresenterTest {
|
||||
val loaded = awaitPollLoaded()
|
||||
assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
assertThat(loaded.showBackConfirmation).isFalse()
|
||||
loaded.eventSink(CreatePollEvents.ConfirmNavBack)
|
||||
loaded.eventSink(CreatePollEvent.ConfirmNavBack)
|
||||
assertThat(navUpInvocationsCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
@@ -455,11 +455,11 @@ class CreatePollPresenterTest {
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
val loaded = awaitPollLoaded()
|
||||
loaded.eventSink(CreatePollEvents.SetQuestion("CHANGED"))
|
||||
loaded.eventSink(CreatePollEvent.SetQuestion("CHANGED"))
|
||||
assertThat(awaitItem().showBackConfirmation).isFalse()
|
||||
loaded.eventSink(CreatePollEvents.ConfirmNavBack)
|
||||
loaded.eventSink(CreatePollEvent.ConfirmNavBack)
|
||||
assertThat(awaitItem().showBackConfirmation).isTrue()
|
||||
loaded.eventSink(CreatePollEvents.HideConfirmation)
|
||||
loaded.eventSink(CreatePollEvent.HideConfirmation)
|
||||
assertThat(awaitItem().showBackConfirmation).isFalse()
|
||||
assertThat(navUpInvocationsCount).isEqualTo(0)
|
||||
}
|
||||
@@ -474,7 +474,7 @@ class CreatePollPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
|
||||
awaitPollLoaded().eventSink(CreatePollEvent.Delete(confirmed = false))
|
||||
awaitDeleteConfirmation()
|
||||
assert(redactEventLambda).isNeverCalled()
|
||||
}
|
||||
@@ -489,8 +489,8 @@ class CreatePollPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
|
||||
awaitDeleteConfirmation().eventSink(CreatePollEvents.HideConfirmation)
|
||||
awaitPollLoaded().eventSink(CreatePollEvent.Delete(confirmed = false))
|
||||
awaitDeleteConfirmation().eventSink(CreatePollEvent.HideConfirmation)
|
||||
awaitPollLoaded().apply {
|
||||
assertThat(showDeleteConfirmation).isFalse()
|
||||
}
|
||||
@@ -507,8 +507,8 @@ class CreatePollPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitDefaultItem()
|
||||
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false))
|
||||
awaitDeleteConfirmation().eventSink(CreatePollEvents.Delete(confirmed = true))
|
||||
awaitPollLoaded().eventSink(CreatePollEvent.Delete(confirmed = false))
|
||||
awaitDeleteConfirmation().eventSink(CreatePollEvent.Delete(confirmed = true))
|
||||
awaitPollLoaded().apply {
|
||||
assertThat(showDeleteConfirmation).isFalse()
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ package io.element.android.features.preferences.impl.user.editprofile
|
||||
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
|
||||
sealed interface EditUserProfileEvents {
|
||||
data class HandleAvatarAction(val action: AvatarAction) : EditUserProfileEvents
|
||||
data class UpdateDisplayName(val name: String) : EditUserProfileEvents
|
||||
data object Exit : EditUserProfileEvents
|
||||
data object Save : EditUserProfileEvents
|
||||
data object CloseDialog : EditUserProfileEvents
|
||||
sealed interface EditUserProfileEvent {
|
||||
data class HandleAvatarAction(val action: AvatarAction) : EditUserProfileEvent
|
||||
data class UpdateDisplayName(val name: String) : EditUserProfileEvent
|
||||
data object Exit : EditUserProfileEvent
|
||||
data object Save : EditUserProfileEvent
|
||||
data object CloseDialog : EditUserProfileEvent
|
||||
}
|
||||
@@ -112,15 +112,15 @@ class EditUserProfilePresenter(
|
||||
!userDisplayName.isNullOrBlank() && hasProfileChanged
|
||||
}
|
||||
|
||||
fun handleEvent(event: EditUserProfileEvents) {
|
||||
fun handleEvent(event: EditUserProfileEvent) {
|
||||
when (event) {
|
||||
is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges(
|
||||
is EditUserProfileEvent.Save -> localCoroutineScope.saveChanges(
|
||||
name = userDisplayName,
|
||||
avatarUri = userAvatarUri?.toUri(),
|
||||
currentUser = matrixUser,
|
||||
action = saveAction,
|
||||
)
|
||||
is EditUserProfileEvents.HandleAvatarAction -> {
|
||||
is EditUserProfileEvent.HandleAvatarAction -> {
|
||||
when (event.action) {
|
||||
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
||||
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
|
||||
@@ -135,8 +135,8 @@ class EditUserProfilePresenter(
|
||||
}
|
||||
}
|
||||
}
|
||||
is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name
|
||||
EditUserProfileEvents.Exit -> {
|
||||
is EditUserProfileEvent.UpdateDisplayName -> userDisplayName = event.name
|
||||
EditUserProfileEvent.Exit -> {
|
||||
when (saveAction.value) {
|
||||
is AsyncAction.Confirming -> {
|
||||
// Close the dialog right now
|
||||
@@ -157,7 +157,7 @@ class EditUserProfilePresenter(
|
||||
}
|
||||
}
|
||||
}
|
||||
EditUserProfileEvents.CloseDialog -> saveAction.value = AsyncAction.Uninitialized
|
||||
EditUserProfileEvent.CloseDialog -> saveAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,5 +22,5 @@ data class EditUserProfileState(
|
||||
val saveButtonEnabled: Boolean,
|
||||
val saveAction: AsyncAction<Unit>,
|
||||
val cameraPermissionState: PermissionsState,
|
||||
val eventSink: (EditUserProfileEvents) -> Unit
|
||||
val eventSink: (EditUserProfileEvent) -> Unit
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ fun aEditUserProfileState(
|
||||
saveButtonEnabled: Boolean = true,
|
||||
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
|
||||
eventSink: (EditUserProfileEvents) -> Unit = {},
|
||||
eventSink: (EditUserProfileEvent) -> Unit = {},
|
||||
) = EditUserProfileState(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
|
||||
@@ -68,7 +68,7 @@ fun EditUserProfileView(
|
||||
|
||||
fun onBackClick() {
|
||||
focusManager.clearFocus()
|
||||
state.eventSink(EditUserProfileEvents.Exit)
|
||||
state.eventSink(EditUserProfileEvent.Exit)
|
||||
}
|
||||
|
||||
BackHandler(
|
||||
@@ -87,7 +87,7 @@ fun EditUserProfileView(
|
||||
enabled = state.saveButtonEnabled,
|
||||
onClick = {
|
||||
focusManager.clearFocus()
|
||||
state.eventSink(EditUserProfileEvents.Save)
|
||||
state.eventSink(EditUserProfileEvent.Save)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -125,7 +125,7 @@ fun EditUserProfileView(
|
||||
value = state.displayName,
|
||||
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
|
||||
singleLine = true,
|
||||
onValueChange = { state.eventSink(EditUserProfileEvents.UpdateDisplayName(it)) },
|
||||
onValueChange = { state.eventSink(EditUserProfileEvent.UpdateDisplayName(it)) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ fun EditUserProfileView(
|
||||
actions = state.avatarActions,
|
||||
isVisible = isAvatarActionsSheetVisible.value,
|
||||
onDismiss = { isAvatarActionsSheetVisible.value = false },
|
||||
onSelectAction = { state.eventSink(EditUserProfileEvents.HandleAvatarAction(it)) }
|
||||
onSelectAction = { state.eventSink(EditUserProfileEvent.HandleAvatarAction(it)) }
|
||||
)
|
||||
|
||||
AsyncActionView(
|
||||
@@ -147,8 +147,9 @@ fun EditUserProfileView(
|
||||
when (confirming) {
|
||||
is AsyncAction.ConfirmingCancellation -> {
|
||||
SaveChangesDialog(
|
||||
onSubmitClick = { state.eventSink(EditUserProfileEvents.Exit) },
|
||||
onDismiss = { state.eventSink(EditUserProfileEvents.CloseDialog) }
|
||||
onSaveClick = { state.eventSink(EditUserProfileEvent.Save) },
|
||||
onDiscardClick = { state.eventSink(EditUserProfileEvent.Exit) },
|
||||
onDismiss = { state.eventSink(EditUserProfileEvent.CloseDialog) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -156,7 +157,7 @@ fun EditUserProfileView(
|
||||
onSuccess = { onEditProfileSuccess() },
|
||||
errorTitle = { stringResource(R.string.screen_edit_profile_error_title) },
|
||||
errorMessage = { stringResource(R.string.screen_edit_profile_error) },
|
||||
onErrorDismiss = { state.eventSink(EditUserProfileEvents.CloseDialog) },
|
||||
onErrorDismiss = { state.eventSink(EditUserProfileEvent.CloseDialog) },
|
||||
)
|
||||
}
|
||||
PermissionsView(
|
||||
|
||||
@@ -124,7 +124,7 @@ class EditUserProfilePresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(EditUserProfileEvents.Exit)
|
||||
initialState.eventSink(EditUserProfileEvent.Exit)
|
||||
closeLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
@@ -139,21 +139,21 @@ class EditUserProfilePresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("New name"))
|
||||
initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("New name"))
|
||||
val withUpdatedName = awaitItem()
|
||||
withUpdatedName.eventSink(EditUserProfileEvents.Exit)
|
||||
withUpdatedName.eventSink(EditUserProfileEvent.Exit)
|
||||
val withConfirmation = awaitItem()
|
||||
assertThat(withConfirmation.saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
|
||||
// Cancel
|
||||
withConfirmation.eventSink(EditUserProfileEvents.CloseDialog)
|
||||
withConfirmation.eventSink(EditUserProfileEvent.CloseDialog)
|
||||
val afterCancel = awaitItem()
|
||||
assertThat(afterCancel.saveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
// Try again and confirm
|
||||
afterCancel.eventSink(EditUserProfileEvents.Exit)
|
||||
afterCancel.eventSink(EditUserProfileEvent.Exit)
|
||||
val withConfirmation2 = awaitItem()
|
||||
assertThat(withConfirmation2.saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
|
||||
closeLambda.assertions().isNeverCalled()
|
||||
withConfirmation2.eventSink(EditUserProfileEvents.Exit)
|
||||
withConfirmation2.eventSink(EditUserProfileEvent.Exit)
|
||||
// Dialog is closed
|
||||
val finalState = awaitItem()
|
||||
assertThat(finalState.saveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
@@ -174,17 +174,17 @@ class EditUserProfilePresenterTest {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.displayName).isEqualTo("Name")
|
||||
assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II"))
|
||||
initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name II"))
|
||||
awaitItem().apply {
|
||||
assertThat(displayName).isEqualTo("Name II")
|
||||
assertThat(userAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
}
|
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name III"))
|
||||
initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name III"))
|
||||
awaitItem().apply {
|
||||
assertThat(displayName).isEqualTo("Name III")
|
||||
assertThat(userAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
}
|
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
|
||||
initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.Remove))
|
||||
awaitItem().apply {
|
||||
assertThat(displayName).isEqualTo("Name III")
|
||||
assertThat(userAvatarUrl).isNull()
|
||||
@@ -205,7 +205,7 @@ class EditUserProfilePresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
awaitItem().apply {
|
||||
assertThat(userAvatarUrl).isEqualTo(ANOTHER_AVATAR_URL)
|
||||
}
|
||||
@@ -229,7 +229,7 @@ class EditUserProfilePresenterTest {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
assertThat(initialState.cameraPermissionState.permissionGranted).isFalse()
|
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||
initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||
val stateWithAskingPermission = awaitItem()
|
||||
assertThat(stateWithAskingPermission.cameraPermissionState.showDialog).isTrue()
|
||||
fakePermissionsPresenter.setPermissionGranted()
|
||||
@@ -239,7 +239,7 @@ class EditUserProfilePresenterTest {
|
||||
assertThat(stateWithNewAvatar.userAvatarUrl).isEqualTo(ANOTHER_AVATAR_URL)
|
||||
// Do it again, no permission is requested
|
||||
fakePickerProvider.givenResult(userAvatarUri)
|
||||
stateWithNewAvatar.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||
stateWithNewAvatar.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||
val stateWithNewAvatar2 = awaitItem()
|
||||
assertThat(stateWithNewAvatar2.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
deleteCallback.assertions().isCalledExactly(2).withSequence(
|
||||
@@ -264,22 +264,22 @@ class EditUserProfilePresenterTest {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.saveButtonEnabled).isFalse()
|
||||
// Once a change is made, the save button is enabled
|
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II"))
|
||||
initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name II"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
}
|
||||
// If it's reverted then the save disables again
|
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name"))
|
||||
initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isFalse()
|
||||
}
|
||||
// Make a change...
|
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
|
||||
initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.Remove))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
}
|
||||
// Revert it...
|
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isFalse()
|
||||
}
|
||||
@@ -305,22 +305,22 @@ class EditUserProfilePresenterTest {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.saveButtonEnabled).isFalse()
|
||||
// Once a change is made, the save button is enabled
|
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II"))
|
||||
initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name II"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
}
|
||||
// If it's reverted then the save disables again
|
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name"))
|
||||
initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isFalse()
|
||||
}
|
||||
// Make a change...
|
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
}
|
||||
// Revert it...
|
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
|
||||
initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.Remove))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isFalse()
|
||||
}
|
||||
@@ -344,9 +344,9 @@ class EditUserProfilePresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("New name"))
|
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
|
||||
initialState.eventSink(EditUserProfileEvents.Save)
|
||||
initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("New name"))
|
||||
initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.Remove))
|
||||
initialState.eventSink(EditUserProfileEvent.Save)
|
||||
consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled }
|
||||
assertThat(matrixClient.setDisplayNameCalled).isTrue()
|
||||
assertThat(matrixClient.removeAvatarCalled).isTrue()
|
||||
@@ -365,8 +365,8 @@ class EditUserProfilePresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(" Name "))
|
||||
initialState.eventSink(EditUserProfileEvents.Save)
|
||||
initialState.eventSink(EditUserProfileEvent.UpdateDisplayName(" Name "))
|
||||
initialState.eventSink(EditUserProfileEvent.Save)
|
||||
consumeItemsUntilTimeout()
|
||||
assertThat(matrixClient.setDisplayNameCalled).isFalse()
|
||||
assertThat(matrixClient.uploadAvatarCalled).isFalse()
|
||||
@@ -384,8 +384,8 @@ class EditUserProfilePresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(""))
|
||||
initialState.eventSink(EditUserProfileEvents.Save)
|
||||
initialState.eventSink(EditUserProfileEvent.UpdateDisplayName(""))
|
||||
initialState.eventSink(EditUserProfileEvent.Save)
|
||||
assertThat(matrixClient.setDisplayNameCalled).isFalse()
|
||||
assertThat(matrixClient.uploadAvatarCalled).isFalse()
|
||||
assertThat(matrixClient.removeAvatarCalled).isFalse()
|
||||
@@ -407,8 +407,8 @@ class EditUserProfilePresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(EditUserProfileEvents.Save)
|
||||
initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(EditUserProfileEvent.Save)
|
||||
consumeItemsUntilPredicate { matrixClient.uploadAvatarCalled }
|
||||
assertThat(matrixClient.uploadAvatarCalled).isTrue()
|
||||
}
|
||||
@@ -429,8 +429,8 @@ class EditUserProfilePresenterTest {
|
||||
fakeMediaPreProcessor.givenResult(Result.failure(RuntimeException("Oh no")))
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(EditUserProfileEvents.Save)
|
||||
initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(EditUserProfileEvent.Save)
|
||||
skipItems(2)
|
||||
assertThat(matrixClient.uploadAvatarCalled).isFalse()
|
||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
@@ -443,7 +443,7 @@ class EditUserProfilePresenterTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenSetDisplayNameResult(Result.failure(RuntimeException("!")))
|
||||
}
|
||||
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.UpdateDisplayName("New name"))
|
||||
saveAndAssertFailure(user, matrixClient, EditUserProfileEvent.UpdateDisplayName("New name"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -452,7 +452,7 @@ class EditUserProfilePresenterTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenRemoveAvatarResult(Result.failure(RuntimeException("!")))
|
||||
}
|
||||
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove))
|
||||
saveAndAssertFailure(user, matrixClient, EditUserProfileEvent.HandleAvatarAction(AvatarAction.Remove))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -462,7 +462,7 @@ class EditUserProfilePresenterTest {
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
givenUploadAvatarResult(Result.failure(RuntimeException("!")))
|
||||
}
|
||||
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
saveAndAssertFailure(user, matrixClient, EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -475,16 +475,16 @@ class EditUserProfilePresenterTest {
|
||||
val presenter = createEditUserProfilePresenter(matrixUser = user, matrixClient = matrixClient)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("foo"))
|
||||
initialState.eventSink(EditUserProfileEvents.Save)
|
||||
initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("foo"))
|
||||
initialState.eventSink(EditUserProfileEvent.Save)
|
||||
skipItems(2)
|
||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
initialState.eventSink(EditUserProfileEvents.CloseDialog)
|
||||
initialState.eventSink(EditUserProfileEvent.CloseDialog)
|
||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveAndAssertFailure(matrixUser: MatrixUser, matrixClient: MatrixClient, event: EditUserProfileEvents) {
|
||||
private suspend fun saveAndAssertFailure(matrixUser: MatrixUser, matrixClient: MatrixClient, event: EditUserProfileEvent) {
|
||||
val presenter = createEditUserProfilePresenter(
|
||||
matrixUser = matrixUser,
|
||||
matrixClient = matrixClient,
|
||||
@@ -495,7 +495,7 @@ class EditUserProfilePresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(event)
|
||||
initialState.eventSink(EditUserProfileEvents.Save)
|
||||
initialState.eventSink(EditUserProfileEvent.Save)
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
|
||||
@@ -34,45 +34,45 @@ class EditUserProfileViewTest {
|
||||
|
||||
@Test
|
||||
fun `clicking on back emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvents>()
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
|
||||
rule.setEditUserProfileView(
|
||||
aEditUserProfileState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.pressBack()
|
||||
eventsRecorder.assertSingle(EditUserProfileEvents.Exit)
|
||||
eventsRecorder.assertSingle(EditUserProfileEvent.Exit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on cancel exit emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvents>()
|
||||
fun `clicking on save from the exit confirmation dialog emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
|
||||
rule.setEditUserProfileView(
|
||||
aEditUserProfileState(
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(EditUserProfileEvents.CloseDialog)
|
||||
rule.clickOn(CommonStrings.action_save, inDialog = true)
|
||||
eventsRecorder.assertSingle(EditUserProfileEvent.Save)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on OK exit emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvents>()
|
||||
fun `clicking on discard exit emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
|
||||
rule.setEditUserProfileView(
|
||||
aEditUserProfileState(
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventsRecorder.assertSingle(EditUserProfileEvents.Exit)
|
||||
rule.clickOn(CommonStrings.action_discard)
|
||||
eventsRecorder.assertSingle(EditUserProfileEvent.Exit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on save emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvents>()
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
|
||||
rule.setEditUserProfileView(
|
||||
aEditUserProfileState(
|
||||
saveButtonEnabled = true,
|
||||
@@ -81,12 +81,12 @@ class EditUserProfileViewTest {
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_save)
|
||||
eventsRecorder.assertSingle(EditUserProfileEvents.Save)
|
||||
eventsRecorder.assertSingle(EditUserProfileEvent.Save)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on avatar opens the bottom sheet dialog`() {
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvents>()
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
|
||||
val actions = listOf(
|
||||
AvatarAction.TakePhoto,
|
||||
AvatarAction.ChoosePhoto,
|
||||
@@ -110,7 +110,7 @@ class EditUserProfileViewTest {
|
||||
|
||||
@Test
|
||||
fun `success invokes the expected callback`() {
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvents>(expectEvents = false)
|
||||
val eventsRecorder = EventsRecorder<EditUserProfileEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setEditUserProfileView(
|
||||
aEditUserProfileState(
|
||||
|
||||
@@ -73,8 +73,7 @@ class ChangeRoomPermissionsPresenter(
|
||||
|
||||
private var initialPermissions by mutableStateOf<RoomPowerLevelsValues?>(null)
|
||||
private var currentPermissions by mutableStateOf<RoomPowerLevelsValues?>(null)
|
||||
private var saveAction by mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||
private var confirmExitAction by mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||
private var saveAction by mutableStateOf<AsyncAction<Boolean>>(AsyncAction.Uninitialized)
|
||||
|
||||
@Composable
|
||||
override fun present(): ChangeRoomPermissionsState {
|
||||
@@ -109,15 +108,14 @@ class ChangeRoomPermissionsPresenter(
|
||||
}
|
||||
is ChangeRoomPermissionsEvent.Save -> coroutineScope.save()
|
||||
is ChangeRoomPermissionsEvent.Exit -> {
|
||||
confirmExitAction = if (!hasChanges || confirmExitAction.isConfirming()) {
|
||||
AsyncAction.Success(Unit)
|
||||
saveAction = if (!hasChanges || saveAction == AsyncAction.ConfirmingCancellation) {
|
||||
AsyncAction.Success(false)
|
||||
} else {
|
||||
AsyncAction.ConfirmingNoParams
|
||||
AsyncAction.ConfirmingCancellation
|
||||
}
|
||||
}
|
||||
is ChangeRoomPermissionsEvent.ResetPendingActions -> {
|
||||
saveAction = AsyncAction.Uninitialized
|
||||
confirmExitAction = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,7 +124,6 @@ class ChangeRoomPermissionsPresenter(
|
||||
itemsBySection = itemsBySection,
|
||||
hasChanges = hasChanges,
|
||||
saveAction = saveAction,
|
||||
confirmExitAction = confirmExitAction,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
@@ -147,7 +144,7 @@ class ChangeRoomPermissionsPresenter(
|
||||
.onSuccess {
|
||||
analyticsService.trackPermissionChangeAnalytics(initialPermissions, updatedRoomPowerLevels)
|
||||
initialPermissions = currentPermissions
|
||||
saveAction = AsyncAction.Success(Unit)
|
||||
saveAction = AsyncAction.Success(true)
|
||||
}
|
||||
.onFailure {
|
||||
saveAction = AsyncAction.Failure(it)
|
||||
|
||||
@@ -23,8 +23,7 @@ data class ChangeRoomPermissionsState(
|
||||
val currentPermissions: RoomPowerLevelsValues?,
|
||||
val itemsBySection: ImmutableMap<RoomPermissionsSection, ImmutableList<RoomPermissionType>>,
|
||||
val hasChanges: Boolean,
|
||||
val saveAction: AsyncAction<Unit>,
|
||||
val confirmExitAction: AsyncAction<Unit>,
|
||||
val saveAction: AsyncAction<Boolean>,
|
||||
val eventSink: (ChangeRoomPermissionsEvent) -> Unit,
|
||||
) {
|
||||
fun selectedRoleForType(type: RoomPermissionType): SelectableRole? {
|
||||
|
||||
@@ -25,7 +25,7 @@ class ChangeRoomPermissionsStateProvider : PreviewParameterProvider<ChangeRoomPe
|
||||
hasChanges = true,
|
||||
saveAction = AsyncAction.Failure(IllegalStateException("Failed to save changes"))
|
||||
),
|
||||
aChangeRoomPermissionsState(hasChanges = true, confirmExitAction = AsyncAction.ConfirmingNoParams),
|
||||
aChangeRoomPermissionsState(hasChanges = true, saveAction = AsyncAction.ConfirmingCancellation),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,15 +33,13 @@ internal fun aChangeRoomPermissionsState(
|
||||
currentPermissions: RoomPowerLevelsValues = previewPermissions(),
|
||||
itemsBySection: Map<RoomPermissionsSection, ImmutableList<RoomPermissionType>> = ChangeRoomPermissionsPresenter.buildItems(false),
|
||||
hasChanges: Boolean = false,
|
||||
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
confirmExitAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
saveAction: AsyncAction<Boolean> = AsyncAction.Uninitialized,
|
||||
eventSink: (ChangeRoomPermissionsEvent) -> Unit = {},
|
||||
) = ChangeRoomPermissionsState(
|
||||
currentPermissions = currentPermissions,
|
||||
itemsBySection = itemsBySection.toImmutableMap(),
|
||||
hasChanges = hasChanges,
|
||||
saveAction = saveAction,
|
||||
confirmExitAction = confirmExitAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
|
||||
@@ -18,9 +18,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.features.rolesandpermissions.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
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.preferences.PreferenceDropdown
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
@@ -91,24 +92,19 @@ fun ChangeRoomPermissionsView(
|
||||
|
||||
AsyncActionView(
|
||||
async = state.saveAction,
|
||||
onSuccess = { onComplete(true) },
|
||||
onErrorDismiss = { state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions) }
|
||||
)
|
||||
|
||||
AsyncActionView(
|
||||
async = state.confirmExitAction,
|
||||
onSuccess = { onComplete(false) },
|
||||
confirmationDialog = {
|
||||
ConfirmationDialog(
|
||||
title = stringResource(R.string.screen_room_change_role_unsaved_changes_title),
|
||||
content = stringResource(R.string.screen_room_change_role_unsaved_changes_description),
|
||||
submitText = stringResource(CommonStrings.action_save),
|
||||
cancelText = stringResource(CommonStrings.action_discard),
|
||||
onSubmitClick = { state.eventSink(ChangeRoomPermissionsEvent.Save) },
|
||||
onDismiss = { state.eventSink(ChangeRoomPermissionsEvent.Exit) }
|
||||
)
|
||||
onSuccess = { onComplete(it) },
|
||||
confirmationDialog = { confirming ->
|
||||
when (confirming) {
|
||||
is AsyncAction.ConfirmingCancellation -> {
|
||||
SaveChangesDialog(
|
||||
onSaveClick = { state.eventSink(ChangeRoomPermissionsEvent.Save) },
|
||||
onDiscardClick = { state.eventSink(ChangeRoomPermissionsEvent.Exit) },
|
||||
onDismiss = { state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions) },
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onErrorDismiss = {},
|
||||
onErrorDismiss = { state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -172,8 +172,9 @@ fun ChangeRolesView(
|
||||
when (confirming) {
|
||||
is AsyncAction.ConfirmingCancellation -> {
|
||||
SaveChangesDialog(
|
||||
onSubmitClick = { state.eventSink(ChangeRolesEvent.Exit) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) }
|
||||
onSaveClick = { state.eventSink(ChangeRolesEvent.Save) },
|
||||
onDiscardClick = { state.eventSink(ChangeRolesEvent.Exit) },
|
||||
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) },
|
||||
)
|
||||
}
|
||||
is ConfirmingModifyingOwners -> {
|
||||
|
||||
@@ -39,7 +39,6 @@ class ChangeRoomPermissionsPresenterTest {
|
||||
assertThat(this.itemsBySection).isNotEmpty()
|
||||
assertThat(this.hasChanges).isFalse()
|
||||
assertThat(this.saveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
assertThat(this.confirmExitAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
// Updated state, permissions loaded
|
||||
@@ -162,7 +161,7 @@ class ChangeRoomPermissionsPresenterTest {
|
||||
assertThat(awaitItem().hasChanges).isFalse()
|
||||
awaitItem().run {
|
||||
assertThat(currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Success(true))
|
||||
}
|
||||
assertThat(analyticsService.capturedEvents).containsExactlyElementsIn(
|
||||
listOf(
|
||||
@@ -243,10 +242,10 @@ class ChangeRoomPermissionsPresenterTest {
|
||||
assertThat(awaitItem().hasChanges).isTrue()
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.Exit)
|
||||
assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||
assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.Exit)
|
||||
assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Success(false))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +259,7 @@ class ChangeRoomPermissionsPresenterTest {
|
||||
|
||||
state.eventSink(ChangeRoomPermissionsEvent.Exit)
|
||||
|
||||
assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Success(false))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
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.clickOnFirst
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
@@ -76,7 +75,7 @@ class ChangeRoomPermissionsViewTest {
|
||||
rule.setChangeRoomPermissionsRule(
|
||||
state = aChangeRoomPermissionsState(
|
||||
hasChanges = true,
|
||||
confirmExitAction = AsyncAction.ConfirmingNoParams,
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = recorder,
|
||||
),
|
||||
)
|
||||
@@ -90,11 +89,11 @@ class ChangeRoomPermissionsViewTest {
|
||||
rule.setChangeRoomPermissionsRule(
|
||||
state = aChangeRoomPermissionsState(
|
||||
hasChanges = true,
|
||||
confirmExitAction = AsyncAction.ConfirmingNoParams,
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = recorder,
|
||||
),
|
||||
)
|
||||
rule.clickOnFirst(CommonStrings.action_save)
|
||||
rule.clickOn(CommonStrings.action_save, inDialog = true)
|
||||
recorder.assertSingle(ChangeRoomPermissionsEvent.Save)
|
||||
}
|
||||
|
||||
@@ -136,9 +135,23 @@ class ChangeRoomPermissionsViewTest {
|
||||
rule.setChangeRoomPermissionsRule(
|
||||
state = aChangeRoomPermissionsState(
|
||||
hasChanges = true,
|
||||
saveAction = AsyncAction.Success(Unit),
|
||||
saveAction = AsyncAction.Success(true),
|
||||
),
|
||||
onComplete = callback
|
||||
onComplete = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_save)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a cancellation exits the screen`() {
|
||||
ensureCalledOnceWithParam(false) { callback ->
|
||||
rule.setChangeRoomPermissionsRule(
|
||||
state = aChangeRoomPermissionsState(
|
||||
hasChanges = true,
|
||||
saveAction = AsyncAction.Success(false),
|
||||
),
|
||||
onComplete = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_save)
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class ChangeRolesViewTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exit confirmation dialog - submit exits the screen`() {
|
||||
fun `exit confirmation dialog - discard exits the screen`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesContent(
|
||||
state = aChangeRolesState(
|
||||
@@ -128,12 +128,12 @@ class ChangeRolesViewTest {
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
rule.clickOn(CommonStrings.action_discard)
|
||||
eventsRecorder.assertSingle(ChangeRolesEvent.Exit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exit confirmation dialog - cancel removes the dialog`() {
|
||||
fun `exit confirmation dialog - save emits the save event`() {
|
||||
val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
|
||||
rule.setChangeRolesContent(
|
||||
state = aChangeRolesState(
|
||||
@@ -142,8 +142,8 @@ class ChangeRolesViewTest {
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog)
|
||||
rule.clickOn(CommonStrings.action_save)
|
||||
eventsRecorder.assertSingle(ChangeRolesEvent.Save)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -383,7 +383,16 @@ class RoomDetailsFlowNode(
|
||||
knockRequestsListEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
NavTarget.SecurityAndPrivacy -> {
|
||||
securityAndPrivacyEntryPoint.createNode(this, buildContext)
|
||||
val callback = object : SecurityAndPrivacyEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
securityAndPrivacyEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
is NavTarget.VerifyUser -> {
|
||||
val params = OutgoingVerificationEntryPoint.Params(
|
||||
|
||||
@@ -10,11 +10,11 @@ package io.element.android.features.roomdetailsedit.impl
|
||||
|
||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||
|
||||
sealed interface RoomDetailsEditEvents {
|
||||
data class HandleAvatarAction(val action: AvatarAction) : RoomDetailsEditEvents
|
||||
data class UpdateRoomName(val name: String) : RoomDetailsEditEvents
|
||||
data class UpdateRoomTopic(val topic: String) : RoomDetailsEditEvents
|
||||
data object OnBackPress : RoomDetailsEditEvents
|
||||
data object Save : RoomDetailsEditEvents
|
||||
data object CloseDialog : RoomDetailsEditEvents
|
||||
sealed interface RoomDetailsEditEvent {
|
||||
data class HandleAvatarAction(val action: AvatarAction) : RoomDetailsEditEvent
|
||||
data class UpdateRoomName(val name: String) : RoomDetailsEditEvent
|
||||
data class UpdateRoomTopic(val topic: String) : RoomDetailsEditEvent
|
||||
data object OnBackPress : RoomDetailsEditEvent
|
||||
data object Save : RoomDetailsEditEvent
|
||||
data object CloseDialog : RoomDetailsEditEvent
|
||||
}
|
||||
@@ -139,9 +139,9 @@ class RoomDetailsEditPresenter(
|
||||
|
||||
val saveAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
fun handleEvent(event: RoomDetailsEditEvents) {
|
||||
fun handleEvent(event: RoomDetailsEditEvent) {
|
||||
when (event) {
|
||||
is RoomDetailsEditEvents.Save -> localCoroutineScope.saveChanges(
|
||||
is RoomDetailsEditEvent.Save -> localCoroutineScope.saveChanges(
|
||||
currentNameTrimmed = roomRawNameTrimmed,
|
||||
newNameTrimmed = roomRawNameEdited.trim(),
|
||||
currentTopicTrimmed = roomTopicTrimmed,
|
||||
@@ -150,7 +150,7 @@ class RoomDetailsEditPresenter(
|
||||
newAvatarUri = roomAvatarUriEdited?.toUri(),
|
||||
action = saveAction,
|
||||
)
|
||||
is RoomDetailsEditEvents.HandleAvatarAction -> {
|
||||
is RoomDetailsEditEvent.HandleAvatarAction -> {
|
||||
when (event.action) {
|
||||
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
||||
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
|
||||
@@ -166,10 +166,10 @@ class RoomDetailsEditPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
is RoomDetailsEditEvents.UpdateRoomName -> roomRawNameEdited = event.name
|
||||
is RoomDetailsEditEvents.UpdateRoomTopic -> roomTopicEdited = event.topic
|
||||
RoomDetailsEditEvents.CloseDialog -> saveAction.value = AsyncAction.Uninitialized
|
||||
RoomDetailsEditEvents.OnBackPress -> if (saveButtonEnabled.not() || saveAction.value == AsyncAction.ConfirmingCancellation) {
|
||||
is RoomDetailsEditEvent.UpdateRoomName -> roomRawNameEdited = event.name
|
||||
is RoomDetailsEditEvent.UpdateRoomTopic -> roomTopicEdited = event.topic
|
||||
RoomDetailsEditEvent.CloseDialog -> saveAction.value = AsyncAction.Uninitialized
|
||||
RoomDetailsEditEvent.OnBackPress -> if (saveButtonEnabled.not() || saveAction.value == AsyncAction.ConfirmingCancellation) {
|
||||
// No changes to save or already confirming exit without saving
|
||||
saveAction.value = AsyncAction.Success(Unit)
|
||||
} else {
|
||||
|
||||
@@ -28,5 +28,5 @@ data class RoomDetailsEditState(
|
||||
val saveAction: AsyncAction<Unit>,
|
||||
val cameraPermissionState: PermissionsState,
|
||||
val isSpace: Boolean,
|
||||
val eventSink: (RoomDetailsEditEvents) -> Unit
|
||||
val eventSink: (RoomDetailsEditEvent) -> Unit
|
||||
)
|
||||
|
||||
@@ -45,7 +45,7 @@ fun aRoomDetailsEditState(
|
||||
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
|
||||
isSpace: Boolean = false,
|
||||
eventSink: (RoomDetailsEditEvents) -> Unit = {},
|
||||
eventSink: (RoomDetailsEditEvent) -> Unit = {},
|
||||
) = RoomDetailsEditState(
|
||||
roomId = roomId,
|
||||
roomRawName = roomRawName,
|
||||
|
||||
@@ -64,7 +64,7 @@ fun RoomDetailsEditView(
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
state.eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||
state.eventSink(RoomDetailsEditEvent.OnBackPress)
|
||||
}
|
||||
Scaffold(
|
||||
modifier = modifier.clearFocusOnTap(focusManager),
|
||||
@@ -74,7 +74,7 @@ fun RoomDetailsEditView(
|
||||
navigationIcon = {
|
||||
BackButton(
|
||||
onClick = {
|
||||
state.eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||
state.eventSink(RoomDetailsEditEvent.OnBackPress)
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -84,7 +84,7 @@ fun RoomDetailsEditView(
|
||||
enabled = state.saveButtonEnabled,
|
||||
onClick = {
|
||||
focusManager.clearFocus()
|
||||
state.eventSink(RoomDetailsEditEvents.Save)
|
||||
state.eventSink(RoomDetailsEditEvent.Save)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -121,7 +121,7 @@ fun RoomDetailsEditView(
|
||||
placeholder = stringResource(CommonStrings.common_room_name_placeholder),
|
||||
singleLine = true,
|
||||
readOnly = !state.canChangeName,
|
||||
onValueChange = { state.eventSink(RoomDetailsEditEvents.UpdateRoomName(it)) },
|
||||
onValueChange = { state.eventSink(RoomDetailsEditEvent.UpdateRoomName(it)) },
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
@@ -136,7 +136,7 @@ fun RoomDetailsEditView(
|
||||
},
|
||||
maxLines = 10,
|
||||
readOnly = !state.canChangeTopic,
|
||||
onValueChange = { state.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(it)) },
|
||||
onValueChange = { state.eventSink(RoomDetailsEditEvent.UpdateRoomTopic(it)) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
@@ -147,7 +147,7 @@ fun RoomDetailsEditView(
|
||||
actions = state.avatarActions,
|
||||
isVisible = isAvatarActionsSheetVisible.value,
|
||||
onDismiss = { isAvatarActionsSheetVisible.value = false },
|
||||
onSelectAction = { state.eventSink(RoomDetailsEditEvents.HandleAvatarAction(it)) }
|
||||
onSelectAction = { state.eventSink(RoomDetailsEditEvent.HandleAvatarAction(it)) }
|
||||
)
|
||||
AsyncActionView(
|
||||
async = state.saveAction,
|
||||
@@ -159,14 +159,15 @@ fun RoomDetailsEditView(
|
||||
confirmationDialog = {
|
||||
if (state.saveAction == AsyncAction.ConfirmingCancellation) {
|
||||
SaveChangesDialog(
|
||||
onSubmitClick = { state.eventSink(RoomDetailsEditEvents.OnBackPress) },
|
||||
onDismiss = { state.eventSink(RoomDetailsEditEvents.CloseDialog) }
|
||||
onSaveClick = { state.eventSink(RoomDetailsEditEvent.Save) },
|
||||
onDiscardClick = { state.eventSink(RoomDetailsEditEvent.OnBackPress) },
|
||||
onDismiss = { state.eventSink(RoomDetailsEditEvent.CloseDialog) }
|
||||
)
|
||||
}
|
||||
},
|
||||
onSuccess = { onDone() },
|
||||
errorMessage = { stringResource(R.string.screen_room_details_edition_error) },
|
||||
onErrorDismiss = { state.eventSink(RoomDetailsEditEvents.CloseDialog) }
|
||||
onErrorDismiss = { state.eventSink(RoomDetailsEditEvent.CloseDialog) }
|
||||
)
|
||||
|
||||
PermissionsView(
|
||||
|
||||
@@ -241,25 +241,25 @@ class RoomDetailsEditPresenterTest {
|
||||
assertThat(initialState.roomTopic).isEqualTo("My topic")
|
||||
assertThat(initialState.roomRawName).isEqualTo("Name")
|
||||
assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name II"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name II"))
|
||||
awaitItem().apply {
|
||||
assertThat(roomTopic).isEqualTo("My topic")
|
||||
assertThat(roomRawName).isEqualTo("Name II")
|
||||
assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
}
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name III"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name III"))
|
||||
awaitItem().apply {
|
||||
assertThat(roomTopic).isEqualTo("My topic")
|
||||
assertThat(roomRawName).isEqualTo("Name III")
|
||||
assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
}
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("Another topic"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("Another topic"))
|
||||
awaitItem().apply {
|
||||
assertThat(roomTopic).isEqualTo("Another topic")
|
||||
assertThat(roomRawName).isEqualTo("Name III")
|
||||
assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
}
|
||||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
|
||||
initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove))
|
||||
awaitItem().apply {
|
||||
assertThat(roomTopic).isEqualTo("Another topic")
|
||||
assertThat(roomRawName).isEqualTo("Name III")
|
||||
@@ -285,7 +285,7 @@ class RoomDetailsEditPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
awaitItem().apply {
|
||||
assertThat(roomAvatarUrl).isEqualTo(anotherAvatarUri.toString())
|
||||
}
|
||||
@@ -312,7 +312,7 @@ class RoomDetailsEditPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
assertThat(initialState.cameraPermissionState.permissionGranted).isFalse()
|
||||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||
initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||
val stateWithAskingPermission = awaitItem()
|
||||
assertThat(stateWithAskingPermission.cameraPermissionState.showDialog).isTrue()
|
||||
fakePermissionsPresenter.setPermissionGranted()
|
||||
@@ -322,7 +322,7 @@ class RoomDetailsEditPresenterTest {
|
||||
assertThat(stateWithNewAvatar.roomAvatarUrl).isEqualTo(anotherAvatarUri.toString())
|
||||
// Do it again, no permission is requested
|
||||
fakePickerProvider.givenResult(roomAvatarUri)
|
||||
stateWithNewAvatar.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||
stateWithNewAvatar.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||
val stateWithNewAvatar2 = awaitItem()
|
||||
assertThat(stateWithNewAvatar2.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
|
||||
deleteCallback.assertions().isCalledExactly(3).withSequence(
|
||||
@@ -351,32 +351,32 @@ class RoomDetailsEditPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.saveButtonEnabled).isFalse()
|
||||
// Once a change is made, the save button is enabled
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name II"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name II"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
}
|
||||
// If it's reverted then the save disables again
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isFalse()
|
||||
}
|
||||
// Make a change...
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("Another topic"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("Another topic"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
}
|
||||
// Revert it...
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("My topic"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("My topic"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isFalse()
|
||||
}
|
||||
// Make a change...
|
||||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
|
||||
initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
}
|
||||
// Revert it...
|
||||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isFalse()
|
||||
}
|
||||
@@ -401,32 +401,32 @@ class RoomDetailsEditPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.saveButtonEnabled).isFalse()
|
||||
// Once a change is made, the save button is enabled
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name II"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name II"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
}
|
||||
// If it's reverted then the save disables again
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("fallback"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("fallback"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isFalse()
|
||||
}
|
||||
// Make a change...
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("Another topic"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("Another topic"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
}
|
||||
// Revert it...
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(""))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic(""))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isFalse()
|
||||
}
|
||||
// Make a change...
|
||||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
}
|
||||
// Revert it...
|
||||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
|
||||
initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isFalse()
|
||||
}
|
||||
@@ -454,10 +454,10 @@ class RoomDetailsEditPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("New name"))
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("New topic"))
|
||||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
|
||||
initialState.eventSink(RoomDetailsEditEvents.Save)
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("New name"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("New topic"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove))
|
||||
initialState.eventSink(RoomDetailsEditEvent.Save)
|
||||
skipItems(5)
|
||||
setNameResult.assertions().isCalledOnce().with(value("New name"))
|
||||
setTopicResult.assertions().isCalledOnce().with(value("New topic"))
|
||||
@@ -480,9 +480,9 @@ class RoomDetailsEditPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName(" Name "))
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(" My topic "))
|
||||
initialState.eventSink(RoomDetailsEditEvents.Save)
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName(" Name "))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic(" My topic "))
|
||||
initialState.eventSink(RoomDetailsEditEvent.Save)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
@@ -502,8 +502,8 @@ class RoomDetailsEditPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(""))
|
||||
initialState.eventSink(RoomDetailsEditEvents.Save)
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic(""))
|
||||
initialState.eventSink(RoomDetailsEditEvent.Save)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
deleteCallback.assertions().isCalledOnce().with(value(null))
|
||||
}
|
||||
@@ -524,8 +524,8 @@ class RoomDetailsEditPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName(""))
|
||||
initialState.eventSink(RoomDetailsEditEvents.Save)
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName(""))
|
||||
initialState.eventSink(RoomDetailsEditEvent.Save)
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
deleteCallback.assertions().isCalledOnce().with(value(null))
|
||||
}
|
||||
@@ -549,8 +549,8 @@ class RoomDetailsEditPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(RoomDetailsEditEvents.Save)
|
||||
initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(RoomDetailsEditEvent.Save)
|
||||
skipItems(4)
|
||||
updateAvatarResult.assertions().isCalledOnce().with(value(MimeTypes.Jpeg), value(fakeFileContents))
|
||||
deleteCallback.assertions().isCalledExactly(2).withSequence(
|
||||
@@ -577,8 +577,8 @@ class RoomDetailsEditPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(RoomDetailsEditEvents.Save)
|
||||
initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(RoomDetailsEditEvent.Save)
|
||||
skipItems(3)
|
||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
}
|
||||
@@ -593,7 +593,7 @@ class RoomDetailsEditPresenterTest {
|
||||
setNameResult = { Result.failure(RuntimeException("!")) },
|
||||
canSendStateResult = { _, _ -> Result.success(true) }
|
||||
)
|
||||
saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomName("New name"), deleteCallbackNumberOfInvocation = 1)
|
||||
saveAndAssertFailure(room, RoomDetailsEditEvent.UpdateRoomName("New name"), deleteCallbackNumberOfInvocation = 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -605,7 +605,7 @@ class RoomDetailsEditPresenterTest {
|
||||
setTopicResult = { Result.failure(RuntimeException("!")) },
|
||||
canSendStateResult = { _, _ -> Result.success(true) }
|
||||
)
|
||||
saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomTopic("New topic"), deleteCallbackNumberOfInvocation = 1)
|
||||
saveAndAssertFailure(room, RoomDetailsEditEvent.UpdateRoomTopic("New topic"), deleteCallbackNumberOfInvocation = 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -617,7 +617,7 @@ class RoomDetailsEditPresenterTest {
|
||||
removeAvatarResult = { Result.failure(RuntimeException("!")) },
|
||||
canSendStateResult = { _, _ -> Result.success(true) }
|
||||
)
|
||||
saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 2)
|
||||
saveAndAssertFailure(room, RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 2)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -630,7 +630,7 @@ class RoomDetailsEditPresenterTest {
|
||||
updateAvatarResult = { _, _ -> Result.failure(RuntimeException("!")) },
|
||||
canSendStateResult = { _, _ -> Result.success(true) }
|
||||
)
|
||||
saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 2)
|
||||
saveAndAssertFailure(room, RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 2)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -650,11 +650,11 @@ class RoomDetailsEditPresenterTest {
|
||||
)
|
||||
presenter.test {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("foo"))
|
||||
initialState.eventSink(RoomDetailsEditEvents.Save)
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("foo"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.Save)
|
||||
skipItems(3)
|
||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
initialState.eventSink(RoomDetailsEditEvents.CloseDialog)
|
||||
initialState.eventSink(RoomDetailsEditEvent.CloseDialog)
|
||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||
}
|
||||
}
|
||||
@@ -674,14 +674,14 @@ class RoomDetailsEditPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.saveButtonEnabled).isFalse()
|
||||
// Once a change is made, the save button is enabled
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name edited"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name edited"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||
eventSink(RoomDetailsEditEvent.OnBackPress)
|
||||
}
|
||||
awaitItem().apply {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
|
||||
eventSink(RoomDetailsEditEvents.CloseDialog)
|
||||
eventSink(RoomDetailsEditEvent.CloseDialog)
|
||||
}
|
||||
awaitItem().apply {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
@@ -702,7 +702,7 @@ class RoomDetailsEditPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.saveButtonEnabled).isFalse()
|
||||
initialState.eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||
initialState.eventSink(RoomDetailsEditEvent.OnBackPress)
|
||||
assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
}
|
||||
}
|
||||
@@ -721,14 +721,14 @@ class RoomDetailsEditPresenterTest {
|
||||
val initialState = awaitFirstItem()
|
||||
assertThat(initialState.saveButtonEnabled).isFalse()
|
||||
// Once a change is made, the save button is enabled
|
||||
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name edited"))
|
||||
initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name edited"))
|
||||
awaitItem().apply {
|
||||
assertThat(saveButtonEnabled).isTrue()
|
||||
eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||
eventSink(RoomDetailsEditEvent.OnBackPress)
|
||||
}
|
||||
awaitItem().apply {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
|
||||
eventSink(RoomDetailsEditEvents.OnBackPress)
|
||||
eventSink(RoomDetailsEditEvent.OnBackPress)
|
||||
}
|
||||
awaitItem().apply {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
@@ -738,7 +738,7 @@ class RoomDetailsEditPresenterTest {
|
||||
|
||||
private suspend fun saveAndAssertFailure(
|
||||
room: JoinedRoom,
|
||||
event: RoomDetailsEditEvents,
|
||||
event: RoomDetailsEditEvent,
|
||||
deleteCallbackNumberOfInvocation: Int = 2,
|
||||
) {
|
||||
val deleteCallback = lambdaRecorder<Uri?, Unit> {}
|
||||
@@ -749,7 +749,7 @@ class RoomDetailsEditPresenterTest {
|
||||
presenter.test {
|
||||
val initialState = awaitFirstItem()
|
||||
initialState.eventSink(event)
|
||||
initialState.eventSink(RoomDetailsEditEvents.Save)
|
||||
initialState.eventSink(RoomDetailsEditEvent.Save)
|
||||
skipItems(1)
|
||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
|
||||
@@ -39,45 +39,45 @@ class RoomDetailsEditViewTest {
|
||||
|
||||
@Test
|
||||
fun `clicking on back emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>()
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.pressBack()
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvents.OnBackPress)
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on OK when confirming exit emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>()
|
||||
fun `clicking on discard when confirming exit emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvents.OnBackPress)
|
||||
rule.clickOn(CommonStrings.action_discard)
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on cancel when confirming exit emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>()
|
||||
fun `clicking on save when confirming exit emits the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvents.CloseDialog)
|
||||
rule.clickOn(CommonStrings.action_save, inDialog = true)
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvent.Save)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when edition is successful, the expected callback is invoked`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>(expectEvents = false)
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
@@ -91,7 +91,7 @@ class RoomDetailsEditViewTest {
|
||||
|
||||
@Test
|
||||
fun `when name is changed, the expected Event is emitted`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>()
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
eventSink = eventsRecorder,
|
||||
@@ -99,12 +99,12 @@ class RoomDetailsEditViewTest {
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText("Marketing").performTextInput("A")
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvents.UpdateRoomName("AMarketing"))
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomName("AMarketing"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when user cannot change name, nothing happen`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>(expectEvents = false)
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
eventSink = eventsRecorder,
|
||||
@@ -117,7 +117,7 @@ class RoomDetailsEditViewTest {
|
||||
|
||||
@Test
|
||||
fun `when topic is changed, the expected Event is emitted`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>()
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
eventSink = eventsRecorder,
|
||||
@@ -125,12 +125,12 @@ class RoomDetailsEditViewTest {
|
||||
),
|
||||
)
|
||||
rule.onNodeWithText("My Topic").performTextInput("A")
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvents.UpdateRoomTopic("AMy Topic"))
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomTopic("AMy Topic"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when user cannot change topic, nothing happen`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>(expectEvents = false)
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
eventSink = eventsRecorder,
|
||||
@@ -146,7 +146,7 @@ class RoomDetailsEditViewTest {
|
||||
fun `when avatar is changed with action to take photo, the expected Event is emitted`() {
|
||||
testAvatarChange(
|
||||
stringActionRes = CommonStrings.action_take_photo,
|
||||
expectedEvent = RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto),
|
||||
expectedEvent = RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.TakePhoto),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ class RoomDetailsEditViewTest {
|
||||
fun `when avatar is changed with action to choose photo, the expected Event is emitted`() {
|
||||
testAvatarChange(
|
||||
stringActionRes = CommonStrings.action_choose_photo,
|
||||
expectedEvent = RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto),
|
||||
expectedEvent = RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -164,15 +164,15 @@ class RoomDetailsEditViewTest {
|
||||
fun `when avatar is changed with action to remove photo, the expected Event is emitted`() {
|
||||
testAvatarChange(
|
||||
stringActionRes = CommonStrings.action_remove,
|
||||
expectedEvent = RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove),
|
||||
expectedEvent = RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove),
|
||||
)
|
||||
}
|
||||
|
||||
private fun testAvatarChange(
|
||||
@StringRes stringActionRes: Int,
|
||||
expectedEvent: RoomDetailsEditEvents.HandleAvatarAction,
|
||||
expectedEvent: RoomDetailsEditEvent.HandleAvatarAction,
|
||||
) {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>()
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
eventSink = eventsRecorder,
|
||||
@@ -187,7 +187,7 @@ class RoomDetailsEditViewTest {
|
||||
|
||||
@Test
|
||||
fun `when user cannot change avatar, nothing happen`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>(expectEvents = false)
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
eventSink = eventsRecorder,
|
||||
@@ -200,7 +200,7 @@ class RoomDetailsEditViewTest {
|
||||
|
||||
@Test
|
||||
fun `when save is clicked, the expected Event is emitted`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>()
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
eventSink = eventsRecorder,
|
||||
@@ -208,12 +208,12 @@ class RoomDetailsEditViewTest {
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_save)
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvents.Save)
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvent.Save)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when save is clicked, but nothing need to be saved, nothing happens`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>(expectEvents = false)
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
eventSink = eventsRecorder,
|
||||
@@ -225,7 +225,7 @@ class RoomDetailsEditViewTest {
|
||||
|
||||
@Test
|
||||
fun `when error is shown, closing the dialog emit the expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>()
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
|
||||
rule.setRoomDetailsEditView(
|
||||
aRoomDetailsEditState(
|
||||
eventSink = eventsRecorder,
|
||||
@@ -233,7 +233,7 @@ class RoomDetailsEditViewTest {
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvents.CloseDialog)
|
||||
eventsRecorder.assertSingle(RoomDetailsEditEvent.CloseDialog)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,19 @@
|
||||
|
||||
package io.element.android.features.securityandprivacy.api
|
||||
|
||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
|
||||
fun interface SecurityAndPrivacyEntryPoint : SimpleFeatureEntryPoint
|
||||
fun interface SecurityAndPrivacyEntryPoint : FeatureEntryPoint {
|
||||
interface Callback : Plugin {
|
||||
fun onDone()
|
||||
}
|
||||
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: Callback,
|
||||
): Node
|
||||
}
|
||||
|
||||
@@ -17,7 +17,11 @@ import io.element.android.libraries.di.RoomScope
|
||||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultSecurityAndPrivacyEntryPoint : SecurityAndPrivacyEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<SecurityAndPrivacyFlowNode>(buildContext)
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: SecurityAndPrivacyEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<SecurityAndPrivacyFlowNode>(buildContext, listOf(callback))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
|
||||
import io.element.android.features.securityandprivacy.impl.editroomaddress.EditRoomAddressNode
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -47,7 +49,8 @@ class SecurityAndPrivacyFlowNode(
|
||||
data object EditRoomAddress : NavTarget
|
||||
}
|
||||
|
||||
private val navigator = BackstackSecurityAndPrivacyNavigator(backstack)
|
||||
private val callback: SecurityAndPrivacyEntryPoint.Callback = callback()
|
||||
private val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack)
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
|
||||
@@ -12,15 +12,22 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
|
||||
|
||||
interface SecurityAndPrivacyNavigator : Plugin {
|
||||
fun onDone()
|
||||
fun openEditRoomAddress()
|
||||
fun closeEditRoomAddress()
|
||||
}
|
||||
|
||||
class BackstackSecurityAndPrivacyNavigator(
|
||||
private val callback: SecurityAndPrivacyEntryPoint.Callback,
|
||||
private val backStack: BackStack<SecurityAndPrivacyFlowNode.NavTarget>
|
||||
) : SecurityAndPrivacyNavigator {
|
||||
override fun onDone() {
|
||||
callback.onDone()
|
||||
}
|
||||
|
||||
override fun openEditRoomAddress() {
|
||||
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress)
|
||||
}
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
|
||||
package io.element.android.features.securityandprivacy.impl.root
|
||||
|
||||
sealed interface SecurityAndPrivacyEvents {
|
||||
data object EditRoomAddress : SecurityAndPrivacyEvents
|
||||
data object Save : SecurityAndPrivacyEvents
|
||||
data object Exit : SecurityAndPrivacyEvents
|
||||
data object DismissExitConfirmation : SecurityAndPrivacyEvents
|
||||
data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvents
|
||||
data object ToggleEncryptionState : SecurityAndPrivacyEvents
|
||||
data object CancelEnableEncryption : SecurityAndPrivacyEvents
|
||||
data object ConfirmEnableEncryption : SecurityAndPrivacyEvents
|
||||
data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvents
|
||||
data object ToggleRoomVisibility : SecurityAndPrivacyEvents
|
||||
data object DismissSaveError : SecurityAndPrivacyEvents
|
||||
sealed interface SecurityAndPrivacyEvent {
|
||||
data object EditRoomAddress : SecurityAndPrivacyEvent
|
||||
data object Save : SecurityAndPrivacyEvent
|
||||
data object Exit : SecurityAndPrivacyEvent
|
||||
data object DismissExitConfirmation : SecurityAndPrivacyEvent
|
||||
data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvent
|
||||
data object ToggleEncryptionState : SecurityAndPrivacyEvent
|
||||
data object CancelEnableEncryption : SecurityAndPrivacyEvent
|
||||
data object ConfirmEnableEncryption : SecurityAndPrivacyEvent
|
||||
data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvent
|
||||
data object ToggleRoomVisibility : SecurityAndPrivacyEvent
|
||||
data object DismissSaveError : SecurityAndPrivacyEvent
|
||||
}
|
||||
@@ -40,7 +40,6 @@ class SecurityAndPrivacyNode(
|
||||
val state by stateFlow.collectAsState()
|
||||
SecurityAndPrivacyView(
|
||||
state = state,
|
||||
onBackClick = this::navigateUp,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ class SecurityAndPrivacyPresenter(
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
|
||||
}.collectAsState(false)
|
||||
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
var confirmExitAction by remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
@@ -109,9 +108,9 @@ class SecurityAndPrivacyPresenter(
|
||||
var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) }
|
||||
val permissions by room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value)
|
||||
|
||||
fun handleEvent(event: SecurityAndPrivacyEvents) {
|
||||
fun handleEvent(event: SecurityAndPrivacyEvent) {
|
||||
when (event) {
|
||||
SecurityAndPrivacyEvents.Save -> {
|
||||
SecurityAndPrivacyEvent.Save -> {
|
||||
coroutineScope.save(
|
||||
saveAction = saveAction,
|
||||
isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory,
|
||||
@@ -119,49 +118,55 @@ class SecurityAndPrivacyPresenter(
|
||||
editedSettings = editedSettings
|
||||
)
|
||||
}
|
||||
is SecurityAndPrivacyEvents.ChangeRoomAccess -> {
|
||||
is SecurityAndPrivacyEvent.ChangeRoomAccess -> {
|
||||
editedRoomAccess = event.roomAccess
|
||||
}
|
||||
is SecurityAndPrivacyEvents.ToggleEncryptionState -> {
|
||||
is SecurityAndPrivacyEvent.ToggleEncryptionState -> {
|
||||
if (editedIsEncrypted) {
|
||||
editedIsEncrypted = false
|
||||
} else {
|
||||
showEnableEncryptionConfirmation = true
|
||||
}
|
||||
}
|
||||
is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> {
|
||||
is SecurityAndPrivacyEvent.ChangeHistoryVisibility -> {
|
||||
editedHistoryVisibility = event.historyVisibility
|
||||
}
|
||||
SecurityAndPrivacyEvents.ToggleRoomVisibility -> {
|
||||
SecurityAndPrivacyEvent.ToggleRoomVisibility -> {
|
||||
editedVisibleInRoomDirectory = when (val edited = editedVisibleInRoomDirectory) {
|
||||
is AsyncData.Success -> AsyncData.Success(!edited.data)
|
||||
else -> edited
|
||||
}
|
||||
}
|
||||
SecurityAndPrivacyEvents.EditRoomAddress -> navigator.openEditRoomAddress()
|
||||
SecurityAndPrivacyEvents.CancelEnableEncryption -> {
|
||||
SecurityAndPrivacyEvent.EditRoomAddress -> navigator.openEditRoomAddress()
|
||||
SecurityAndPrivacyEvent.CancelEnableEncryption -> {
|
||||
showEnableEncryptionConfirmation = false
|
||||
}
|
||||
SecurityAndPrivacyEvents.ConfirmEnableEncryption -> {
|
||||
SecurityAndPrivacyEvent.ConfirmEnableEncryption -> {
|
||||
showEnableEncryptionConfirmation = false
|
||||
editedIsEncrypted = true
|
||||
}
|
||||
SecurityAndPrivacyEvents.DismissSaveError -> {
|
||||
SecurityAndPrivacyEvent.DismissSaveError -> {
|
||||
saveAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
SecurityAndPrivacyEvents.Exit -> {
|
||||
confirmExitAction = if (savedSettings == editedSettings || confirmExitAction.isConfirming()) {
|
||||
SecurityAndPrivacyEvent.Exit -> {
|
||||
saveAction.value = if (savedSettings == editedSettings || saveAction.value == AsyncAction.ConfirmingCancellation) {
|
||||
AsyncAction.Success(Unit)
|
||||
} else {
|
||||
AsyncAction.ConfirmingNoParams
|
||||
AsyncAction.ConfirmingCancellation
|
||||
}
|
||||
}
|
||||
SecurityAndPrivacyEvents.DismissExitConfirmation -> {
|
||||
confirmExitAction = AsyncAction.Uninitialized
|
||||
SecurityAndPrivacyEvent.DismissExitConfirmation -> {
|
||||
saveAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(saveAction.value.isSuccess()) {
|
||||
if (saveAction.value.isSuccess()) {
|
||||
navigator.onDone()
|
||||
}
|
||||
}
|
||||
|
||||
val state = SecurityAndPrivacyState(
|
||||
savedSettings = savedSettings,
|
||||
editedSettings = editedSettings,
|
||||
@@ -171,7 +176,6 @@ class SecurityAndPrivacyPresenter(
|
||||
saveAction = saveAction.value,
|
||||
permissions = permissions,
|
||||
isSpace = roomInfo.isSpace,
|
||||
confirmExitAction = confirmExitAction,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
|
||||
|
||||
@@ -22,10 +22,9 @@ data class SecurityAndPrivacyState(
|
||||
val showEnableEncryptionConfirmation: Boolean,
|
||||
val isKnockEnabled: Boolean,
|
||||
val saveAction: AsyncAction<Unit>,
|
||||
val confirmExitAction: AsyncAction<Unit>,
|
||||
val isSpace: Boolean,
|
||||
private val permissions: SecurityAndPrivacyPermissions,
|
||||
val eventSink: (SecurityAndPrivacyEvents) -> Unit
|
||||
val eventSink: (SecurityAndPrivacyEvent) -> Unit
|
||||
) {
|
||||
val canBeSaved = savedSettings != editedSettings
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAn
|
||||
isSpace = false,
|
||||
),
|
||||
aSecurityAndPrivacyState(
|
||||
confirmExitAction = AsyncAction.ConfirmingCancellation,
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
isSpace = false,
|
||||
),
|
||||
aSecurityAndPrivacyState(
|
||||
@@ -109,7 +109,6 @@ fun aSecurityAndPrivacyState(
|
||||
homeserverName: String = "myserver.xyz",
|
||||
showEncryptionConfirmation: Boolean = false,
|
||||
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
confirmExitAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
permissions: SecurityAndPrivacyPermissions = SecurityAndPrivacyPermissions(
|
||||
canChangeRoomAccess = true,
|
||||
canChangeHistoryVisibility = true,
|
||||
@@ -118,14 +117,13 @@ fun aSecurityAndPrivacyState(
|
||||
),
|
||||
isKnockEnabled: Boolean = true,
|
||||
isSpace: Boolean = false,
|
||||
eventSink: (SecurityAndPrivacyEvents) -> Unit = {}
|
||||
eventSink: (SecurityAndPrivacyEvent) -> Unit = {}
|
||||
) = SecurityAndPrivacyState(
|
||||
editedSettings = editedSettings,
|
||||
savedSettings = savedSettings,
|
||||
homeserverName = homeserverName,
|
||||
showEnableEncryptionConfirmation = showEncryptionConfirmation,
|
||||
saveAction = saveAction,
|
||||
confirmExitAction = confirmExitAction,
|
||||
isKnockEnabled = isKnockEnabled,
|
||||
permissions = permissions,
|
||||
isSpace = isSpace,
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.securityandprivacy.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
@@ -56,11 +57,10 @@ import kotlinx.collections.immutable.ImmutableSet
|
||||
@Composable
|
||||
fun SecurityAndPrivacyView(
|
||||
state: SecurityAndPrivacyState,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler {
|
||||
state.eventSink(SecurityAndPrivacyEvents.Exit)
|
||||
state.eventSink(SecurityAndPrivacyEvent.Exit)
|
||||
}
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
@@ -68,10 +68,10 @@ fun SecurityAndPrivacyView(
|
||||
SecurityAndPrivacyToolbar(
|
||||
isSaveActionEnabled = state.canBeSaved,
|
||||
onBackClick = {
|
||||
state.eventSink(SecurityAndPrivacyEvents.Exit)
|
||||
state.eventSink(SecurityAndPrivacyEvent.Exit)
|
||||
},
|
||||
onSaveClick = {
|
||||
state.eventSink(SecurityAndPrivacyEvents.Save)
|
||||
state.eventSink(SecurityAndPrivacyEvent.Save)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -90,7 +90,7 @@ fun SecurityAndPrivacyView(
|
||||
edited = state.editedSettings.roomAccess,
|
||||
saved = state.savedSettings.roomAccess,
|
||||
isKnockEnabled = state.isKnockEnabled,
|
||||
onSelectOption = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) },
|
||||
onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(it)) },
|
||||
)
|
||||
}
|
||||
if (state.showRoomVisibilitySections) {
|
||||
@@ -98,10 +98,10 @@ fun SecurityAndPrivacyView(
|
||||
RoomAddressSection(
|
||||
roomAddress = state.editedSettings.address,
|
||||
homeserverName = state.homeserverName,
|
||||
onRoomAddressClick = { state.eventSink(SecurityAndPrivacyEvents.EditRoomAddress) },
|
||||
onRoomAddressClick = { state.eventSink(SecurityAndPrivacyEvent.EditRoomAddress) },
|
||||
isVisibleInRoomDirectory = state.editedSettings.isVisibleInRoomDirectory,
|
||||
onVisibilityChange = {
|
||||
state.eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility)
|
||||
state.eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -110,10 +110,10 @@ fun SecurityAndPrivacyView(
|
||||
isRoomEncrypted = state.editedSettings.isEncrypted,
|
||||
// encryption can't be disabled once enabled
|
||||
canToggleEncryption = !state.savedSettings.isEncrypted,
|
||||
onToggleEncryption = { state.eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) },
|
||||
onToggleEncryption = { state.eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState) },
|
||||
showConfirmation = state.showEnableEncryptionConfirmation,
|
||||
onDismissConfirmation = { state.eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) },
|
||||
onConfirmEncryption = { state.eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) },
|
||||
onDismissConfirmation = { state.eventSink(SecurityAndPrivacyEvent.CancelEnableEncryption) },
|
||||
onConfirmEncryption = { state.eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption) },
|
||||
)
|
||||
}
|
||||
if (state.showHistoryVisibilitySection) {
|
||||
@@ -121,7 +121,7 @@ fun SecurityAndPrivacyView(
|
||||
editedOption = state.editedSettings.historyVisibility,
|
||||
savedOptions = state.savedSettings.historyVisibility,
|
||||
availableOptions = state.availableHistoryVisibilities,
|
||||
onSelectOption = { state.eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(it)) },
|
||||
onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -129,25 +129,24 @@ fun SecurityAndPrivacyView(
|
||||
AsyncActionView(
|
||||
async = state.saveAction,
|
||||
onSuccess = { },
|
||||
onErrorDismiss = { state.eventSink(SecurityAndPrivacyEvents.DismissSaveError) },
|
||||
onErrorDismiss = { state.eventSink(SecurityAndPrivacyEvent.DismissSaveError) },
|
||||
confirmationDialog = { confirming ->
|
||||
when (confirming) {
|
||||
is AsyncAction.ConfirmingCancellation ->
|
||||
SaveChangesDialog(
|
||||
onSaveClick = { state.eventSink(SecurityAndPrivacyEvent.Save) },
|
||||
onDiscardClick = { state.eventSink(SecurityAndPrivacyEvent.Exit) },
|
||||
onDismiss = { state.eventSink(SecurityAndPrivacyEvent.DismissExitConfirmation) }
|
||||
)
|
||||
}
|
||||
},
|
||||
errorMessage = { stringResource(CommonStrings.error_unknown) },
|
||||
progressDialog = {
|
||||
AsyncActionViewDefaults.ProgressDialog(
|
||||
progressText = stringResource(CommonStrings.common_saving),
|
||||
)
|
||||
},
|
||||
onRetry = { state.eventSink(SecurityAndPrivacyEvents.Save) },
|
||||
)
|
||||
AsyncActionView(
|
||||
async = state.confirmExitAction,
|
||||
onSuccess = { onBackClick() },
|
||||
onErrorDismiss = { },
|
||||
confirmationDialog = {
|
||||
SaveChangesDialog(
|
||||
onSubmitClick = { state.eventSink(SecurityAndPrivacyEvents.Exit) },
|
||||
onDismiss = { state.eventSink(SecurityAndPrivacyEvents.DismissExitConfirmation) }
|
||||
)
|
||||
},
|
||||
onRetry = { state.eventSink(SecurityAndPrivacyEvent.Save) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -425,6 +424,5 @@ internal fun SecurityAndPrivacyViewDarkPreview(@PreviewParameter(SecurityAndPriv
|
||||
private fun ContentToPreview(state: SecurityAndPrivacyState) {
|
||||
SecurityAndPrivacyView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,9 +11,14 @@ package io.element.android.features.securityandprivacy.impl
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeSecurityAndPrivacyNavigator(
|
||||
private val onDoneLambda: () -> Unit = { lambdaError() },
|
||||
private val openEditRoomAddressLambda: () -> Unit = { lambdaError() },
|
||||
private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() },
|
||||
) : SecurityAndPrivacyNavigator {
|
||||
override fun onDone() {
|
||||
onDoneLambda()
|
||||
}
|
||||
|
||||
override fun openEditRoomAddress() {
|
||||
openEditRoomAddressLambda()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
package io.element.android.features.securityandprivacy.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyEvents
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyEvent
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyHistoryVisibility
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyPresenter
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess
|
||||
@@ -96,13 +96,13 @@ class SecurityAndPrivacyPresenterTest {
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
|
||||
assertThat(showRoomVisibilitySections).isFalse()
|
||||
eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone)
|
||||
assertThat(showRoomVisibilitySections).isTrue()
|
||||
assertThat(canBeSaved).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly))
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
|
||||
@@ -119,12 +119,12 @@ class SecurityAndPrivacyPresenterTest {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection)
|
||||
eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceInvite))
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceInvite))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceInvite)
|
||||
assertThat(canBeSaved).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection))
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection)
|
||||
@@ -140,26 +140,26 @@ class SecurityAndPrivacyPresenterTest {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isEncrypted).isFalse()
|
||||
eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState)
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(showEnableEncryptionConfirmation).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption)
|
||||
eventSink(SecurityAndPrivacyEvent.CancelEnableEncryption)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(showEnableEncryptionConfirmation).isFalse()
|
||||
eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState)
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(showEnableEncryptionConfirmation).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption)
|
||||
eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
|
||||
}
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isEncrypted).isTrue()
|
||||
assertThat(showEnableEncryptionConfirmation).isFalse()
|
||||
assertThat(canBeSaved).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState)
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState)
|
||||
}
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
@@ -186,12 +186,12 @@ class SecurityAndPrivacyPresenterTest {
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false))
|
||||
eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility)
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true))
|
||||
assertThat(canBeSaved).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility)
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false))
|
||||
@@ -203,12 +203,12 @@ class SecurityAndPrivacyPresenterTest {
|
||||
@Test
|
||||
fun `present - edit room address`() = runTest {
|
||||
val openEditRoomAddressLambda = lambdaRecorder<Unit> { }
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda)
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda = openEditRoomAddressLambda)
|
||||
val presenter = createSecurityAndPrivacyPresenter(navigator = navigator)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
eventSink(SecurityAndPrivacyEvents.EditRoomAddress)
|
||||
eventSink(SecurityAndPrivacyEvent.EditRoomAddress)
|
||||
}
|
||||
assert(openEditRoomAddressLambda).isCalledOnce()
|
||||
}
|
||||
@@ -231,28 +231,35 @@ class SecurityAndPrivacyPresenterTest {
|
||||
updateRoomVisibilityResult = updateRoomVisibilityLambda,
|
||||
updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda,
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||
val onDoneLambda = lambdaRecorder<Unit> { }
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(
|
||||
onDoneLambda = onDoneLambda,
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(
|
||||
room = room,
|
||||
navigator = navigator,
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
|
||||
eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone))
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone)
|
||||
eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption)
|
||||
eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
|
||||
}
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isEncrypted).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility)
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true))
|
||||
eventSink(SecurityAndPrivacyEvents.Save)
|
||||
eventSink(SecurityAndPrivacyEvent.Save)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Loading)
|
||||
@@ -276,6 +283,7 @@ class SecurityAndPrivacyPresenterTest {
|
||||
assert(updateJoinRuleLambda).isCalledOnce()
|
||||
assert(updateRoomVisibilityLambda).isCalledOnce()
|
||||
assert(updateRoomHistoryVisibilityLambda).isCalledOnce()
|
||||
onDoneLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,23 +311,23 @@ class SecurityAndPrivacyPresenterTest {
|
||||
skipItems(2)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
|
||||
eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone))
|
||||
eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone))
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone)
|
||||
eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption)
|
||||
eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
|
||||
}
|
||||
skipItems(1)
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isEncrypted).isTrue()
|
||||
eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility)
|
||||
eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true))
|
||||
eventSink(SecurityAndPrivacyEvents.Save)
|
||||
eventSink(SecurityAndPrivacyEvent.Save)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Loading)
|
||||
@@ -344,7 +352,7 @@ class SecurityAndPrivacyPresenterTest {
|
||||
assert(updateRoomVisibilityLambda).isCalledOnce()
|
||||
assert(updateRoomHistoryVisibilityLambda).isCalledOnce()
|
||||
// Clear error
|
||||
state.eventSink(SecurityAndPrivacyEvents.DismissSaveError)
|
||||
state.eventSink(SecurityAndPrivacyEvent.DismissSaveError)
|
||||
with(awaitItem()) {
|
||||
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ 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.securityandprivacy.impl.root.SecurityAndPrivacyEvents
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyEvent
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyHistoryVisibility
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyState
|
||||
@@ -24,7 +24,6 @@ import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPriv
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
@@ -40,53 +39,53 @@ class SecurityAndPrivacyViewTest {
|
||||
|
||||
@Test
|
||||
fun `click on back invokes emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.pressBack()
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.Exit)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.Exit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirm cancellation emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
fun `discard cancellation emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
confirmExitAction = AsyncAction.ConfirmingCancellation,
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = recorder,
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.Exit)
|
||||
rule.clickOn(CommonStrings.action_discard)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.Exit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dismiss cancellation confirmation emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
fun `save cancellation confirmation emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
confirmExitAction = AsyncAction.ConfirmingCancellation,
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = recorder,
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.DismissExitConfirmation)
|
||||
rule.clickOn(CommonStrings.action_save, inDialog = true)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.Save)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on room access item emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(R.string.screen_security_and_privacy_room_access_invite_only_option_title)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly))
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on disabled save doesn't emit event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>(expectEvents = false)
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>(expectEvents = false)
|
||||
val state = aSecurityAndPrivacyState(eventSink = recorder)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(CommonStrings.action_save)
|
||||
@@ -95,7 +94,7 @@ class SecurityAndPrivacyViewTest {
|
||||
|
||||
@Test
|
||||
fun `click on enabled save emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
editedSettings = aSecurityAndPrivacySettings(
|
||||
@@ -104,14 +103,14 @@ class SecurityAndPrivacyViewTest {
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(CommonStrings.action_save)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.Save)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.Save)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h640dp")
|
||||
fun `click on room address item emits the expected event`() {
|
||||
val address = "@alias:matrix.org"
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
editedSettings = aSecurityAndPrivacySettings(
|
||||
@@ -121,13 +120,13 @@ class SecurityAndPrivacyViewTest {
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.onNodeWithText(address).performClick()
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.EditRoomAddress)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.EditRoomAddress)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h1024dp")
|
||||
fun `click on room visibility item emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
editedSettings = aSecurityAndPrivacySettings(
|
||||
@@ -137,13 +136,13 @@ class SecurityAndPrivacyViewTest {
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.ToggleRoomVisibility)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.ToggleRoomVisibility)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h640dp")
|
||||
fun `click on history visibility item emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
editedSettings = aSecurityAndPrivacySettings(
|
||||
@@ -152,32 +151,32 @@ class SecurityAndPrivacyViewTest {
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(R.string.screen_security_and_privacy_room_history_since_selecting_option_title)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection))
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "h640dp")
|
||||
fun `click on encryption item emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
savedSettings = aSecurityAndPrivacySettings(isEncrypted = false),
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(R.string.screen_security_and_privacy_encryption_toggle_title)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.ToggleEncryptionState)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.ToggleEncryptionState)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on encryption confirm emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
eventSink = recorder,
|
||||
showEncryptionConfirmation = true,
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.ConfirmEnableEncryption)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,12 +184,10 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSecur
|
||||
state: SecurityAndPrivacyState = aSecurityAndPrivacyState(
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
SecurityAndPrivacyView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@ import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntr
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeSecurityAndPrivacyEntryPoint : SecurityAndPrivacyEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: SecurityAndPrivacyEntryPoint.Callback,
|
||||
): Node {
|
||||
lambdaError()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
@@ -95,9 +96,15 @@ class SpaceSettingsFlowNode(
|
||||
)
|
||||
}
|
||||
is NavTarget.SecurityAndPrivacy -> {
|
||||
val callback = object : SecurityAndPrivacyEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
securityAndPrivacyEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
is NavTarget.RolesAndPermissions -> {
|
||||
|
||||
@@ -17,16 +17,22 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun SaveChangesDialog(
|
||||
onSubmitClick: () -> Unit,
|
||||
onSaveClick: () -> Unit,
|
||||
onDiscardClick: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = stringResource(CommonStrings.dialog_unsaved_changes_title),
|
||||
content: String = stringResource(CommonStrings.dialog_unsaved_changes_description_android),
|
||||
content: String = stringResource(CommonStrings.dialog_unsaved_changes_description),
|
||||
submitText: String = stringResource(CommonStrings.action_save),
|
||||
cancelText: String = stringResource(CommonStrings.action_discard),
|
||||
) = ConfirmationDialog(
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
content = content,
|
||||
onSubmitClick = onSubmitClick,
|
||||
submitText = submitText,
|
||||
cancelText = cancelText,
|
||||
onSubmitClick = onSaveClick,
|
||||
onCancelClick = onDiscardClick,
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
|
||||
@@ -34,7 +40,8 @@ fun SaveChangesDialog(
|
||||
@Composable
|
||||
internal fun SaveChangesDialogPreview() = ElementPreview {
|
||||
SaveChangesDialog(
|
||||
onSubmitClick = {},
|
||||
onSaveClick = {},
|
||||
onDiscardClick = {},
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<string name="notification_room_invite_body_with_sender">"%1$s invited you to join the room"</string>
|
||||
<string name="notification_sender_me">"Me"</string>
|
||||
<string name="notification_sender_mention_reply">"%1$s mentioned or replied"</string>
|
||||
<string name="notification_space_invite_body">"Invited you to join the space"</string>
|
||||
<string name="notification_test_push_notification_content">"You are viewing the notification! Click me!"</string>
|
||||
<string name="notification_thread_in_room">"Thread in %1$s"</string>
|
||||
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string>
|
||||
|
||||
@@ -392,6 +392,7 @@ Are you sure you want to continue?"</string>
|
||||
<string name="dialog_title_error">"Error"</string>
|
||||
<string name="dialog_title_success">"Success"</string>
|
||||
<string name="dialog_title_warning">"Warning"</string>
|
||||
<string name="dialog_unsaved_changes_description">"You have unsaved changes."</string>
|
||||
<string name="dialog_unsaved_changes_description_android">"Your changes have not been saved. Are you sure you want to go back?"</string>
|
||||
<string name="dialog_unsaved_changes_title">"Save changes?"</string>
|
||||
<string name="dialog_video_quality_selector_subtitle_file_size">"The max file size allowed is: %1$s"</string>
|
||||
|
||||
@@ -10,34 +10,33 @@ package io.element.android.tests.testutils
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.onFirst
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import org.junit.rules.TestRule
|
||||
|
||||
fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.clickOn(@StringRes res: Int) {
|
||||
val trueMatcher = SemanticsMatcher("true matcher") { true }
|
||||
|
||||
fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.clickOn(
|
||||
@StringRes res: Int,
|
||||
inDialog: Boolean = false,
|
||||
) {
|
||||
val text = activity.getString(res)
|
||||
onNode(hasText(text) and hasClickAction())
|
||||
onNode(
|
||||
hasText(text) and hasClickAction() and if (inDialog) hasAnyAncestor(isDialog()) else trueMatcher
|
||||
)
|
||||
.performClick()
|
||||
}
|
||||
|
||||
fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.clickOnFirst(@StringRes res: Int) {
|
||||
val text = activity.getString(res)
|
||||
onAllNodes(hasText(text) and hasClickAction()).onFirst().performClick()
|
||||
}
|
||||
|
||||
fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.clickOnLast(@StringRes res: Int) {
|
||||
val text = activity.getString(res)
|
||||
onAllNodes(hasText(text) and hasClickAction()).onFirst().performClick()
|
||||
}
|
||||
|
||||
/**
|
||||
* Press the back button in the app bar.
|
||||
*/
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user