Merge pull request #5845 from element-hq/feature/bma/unsavedChangeDialog

Update unsaved change dialog
This commit is contained in:
Benoit Marty 2025-12-04 11:03:42 +01:00 committed by GitHub
commit fbaedd017e
59 changed files with 537 additions and 468 deletions

View file

@ -10,15 +10,15 @@ package io.element.android.features.poll.impl.create
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
sealed interface CreatePollEvents { sealed interface CreatePollEvent {
data object Save : CreatePollEvents data object Save : CreatePollEvent
data class Delete(val confirmed: Boolean) : CreatePollEvents data class Delete(val confirmed: Boolean) : CreatePollEvent
data class SetQuestion(val question: String) : CreatePollEvents data class SetQuestion(val question: String) : CreatePollEvent
data class SetAnswer(val index: Int, val text: String) : CreatePollEvents data class SetAnswer(val index: Int, val text: String) : CreatePollEvent
data object AddAnswer : CreatePollEvents data object AddAnswer : CreatePollEvent
data class RemoveAnswer(val index: Int) : CreatePollEvents data class RemoveAnswer(val index: Int) : CreatePollEvent
data class SetPollKind(val pollKind: PollKind) : CreatePollEvents data class SetPollKind(val pollKind: PollKind) : CreatePollEvent
data object NavBack : CreatePollEvents data object NavBack : CreatePollEvent
data object ConfirmNavBack : CreatePollEvents data object ConfirmNavBack : CreatePollEvent
data object HideConfirmation : CreatePollEvents data object HideConfirmation : CreatePollEvent
} }

View file

@ -97,9 +97,9 @@ class CreatePollPresenter(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
fun handleEvent(event: CreatePollEvents) { fun handleEvent(event: CreatePollEvent) {
when (event) { when (event) {
is CreatePollEvents.Save -> scope.launch { is CreatePollEvent.Save -> scope.launch {
if (canSave) { if (canSave) {
repository.savePoll( repository.savePoll(
existingPollId = when (mode) { existingPollId = when (mode) {
@ -123,7 +123,7 @@ class CreatePollPresenter(
Timber.d("Cannot create poll") Timber.d("Cannot create poll")
} }
} }
is CreatePollEvents.Delete -> { is CreatePollEvent.Delete -> {
if (mode !is CreatePollMode.EditPoll) { if (mode !is CreatePollMode.EditPoll) {
return return
} }
@ -139,25 +139,25 @@ class CreatePollPresenter(
navigateUp() navigateUp()
} }
} }
is CreatePollEvents.AddAnswer -> { is CreatePollEvent.AddAnswer -> {
poll = poll.withNewAnswer() poll = poll.withNewAnswer()
} }
is CreatePollEvents.RemoveAnswer -> { is CreatePollEvent.RemoveAnswer -> {
poll = poll.withAnswerRemoved(event.index) poll = poll.withAnswerRemoved(event.index)
} }
is CreatePollEvents.SetAnswer -> { is CreatePollEvent.SetAnswer -> {
poll = poll.withAnswerChanged(event.index, event.text) poll = poll.withAnswerChanged(event.index, event.text)
} }
is CreatePollEvents.SetPollKind -> { is CreatePollEvent.SetPollKind -> {
poll = poll.copy(isDisclosed = event.pollKind.isDisclosed) poll = poll.copy(isDisclosed = event.pollKind.isDisclosed)
} }
is CreatePollEvents.SetQuestion -> { is CreatePollEvent.SetQuestion -> {
poll = poll.copy(question = event.question) poll = poll.copy(question = event.question)
} }
is CreatePollEvents.NavBack -> { is CreatePollEvent.NavBack -> {
navigateUp() navigateUp()
} }
CreatePollEvents.ConfirmNavBack -> { CreatePollEvent.ConfirmNavBack -> {
val shouldConfirm = isDirty val shouldConfirm = isDirty
if (shouldConfirm) { if (shouldConfirm) {
showBackConfirmation = true showBackConfirmation = true
@ -165,7 +165,7 @@ class CreatePollPresenter(
navigateUp() navigateUp()
} }
} }
is CreatePollEvents.HideConfirmation -> { is CreatePollEvent.HideConfirmation -> {
showBackConfirmation = false showBackConfirmation = false
showDeleteConfirmation = false showDeleteConfirmation = false
} }

View file

@ -20,7 +20,7 @@ data class CreatePollState(
val pollKind: PollKind, val pollKind: PollKind,
val showBackConfirmation: Boolean, val showBackConfirmation: Boolean,
val showDeleteConfirmation: Boolean, val showDeleteConfirmation: Boolean,
val eventSink: (CreatePollEvents) -> Unit, val eventSink: (CreatePollEvent) -> Unit,
) { ) {
enum class Mode { enum class Mode {
New, New,

View file

@ -62,20 +62,21 @@ fun CreatePollView(
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) } val navBack = { state.eventSink(CreatePollEvent.ConfirmNavBack) }
BackHandler(onBack = navBack) BackHandler(onBack = navBack)
if (state.showBackConfirmation) { if (state.showBackConfirmation) {
SaveChangesDialog( SaveChangesDialog(
onSubmitClick = { state.eventSink(CreatePollEvents.NavBack) }, onSaveClick = { state.eventSink(CreatePollEvent.Save) },
onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } onDiscardClick = { state.eventSink(CreatePollEvent.NavBack) },
onDismiss = { state.eventSink(CreatePollEvent.HideConfirmation) },
) )
} }
if (state.showDeleteConfirmation) { if (state.showDeleteConfirmation) {
ConfirmationDialog( ConfirmationDialog(
title = stringResource(id = R.string.screen_edit_poll_delete_confirmation_title), title = stringResource(id = R.string.screen_edit_poll_delete_confirmation_title),
content = stringResource(id = R.string.screen_edit_poll_delete_confirmation), content = stringResource(id = R.string.screen_edit_poll_delete_confirmation),
onSubmitClick = { state.eventSink(CreatePollEvents.Delete(confirmed = true)) }, onSubmitClick = { state.eventSink(CreatePollEvent.Delete(confirmed = true)) },
onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } onDismiss = { state.eventSink(CreatePollEvent.HideConfirmation) }
) )
} }
val questionFocusRequester = remember { FocusRequester() } val questionFocusRequester = remember { FocusRequester() }
@ -90,7 +91,7 @@ fun CreatePollView(
mode = state.mode, mode = state.mode,
saveEnabled = state.canSave, saveEnabled = state.canSave,
onBackClick = navBack, onBackClick = navBack,
onSaveClick = { state.eventSink(CreatePollEvents.Save) } onSaveClick = { state.eventSink(CreatePollEvent.Save) }
) )
}, },
) { paddingValues -> ) { paddingValues ->
@ -111,7 +112,7 @@ fun CreatePollView(
label = stringResource(id = R.string.screen_create_poll_question_desc), label = stringResource(id = R.string.screen_create_poll_question_desc),
value = state.question, value = state.question,
onValueChange = { onValueChange = {
state.eventSink(CreatePollEvents.SetQuestion(it)) state.eventSink(CreatePollEvent.SetQuestion(it))
}, },
modifier = Modifier modifier = Modifier
.focusRequester(questionFocusRequester) .focusRequester(questionFocusRequester)
@ -130,7 +131,7 @@ fun CreatePollView(
TextField( TextField(
value = answer.text, value = answer.text,
onValueChange = { onValueChange = {
state.eventSink(CreatePollEvents.SetAnswer(index, it)) state.eventSink(CreatePollEvent.SetAnswer(index, it))
}, },
modifier = Modifier modifier = Modifier
.then(if (isLastItem) Modifier.focusRequester(answerFocusRequester) else Modifier) .then(if (isLastItem) Modifier.focusRequester(answerFocusRequester) else Modifier)
@ -144,7 +145,7 @@ fun CreatePollView(
imageVector = CompoundIcons.Delete(), imageVector = CompoundIcons.Delete(),
contentDescription = stringResource(R.string.screen_create_poll_delete_option_a11y, answer.text), contentDescription = stringResource(R.string.screen_create_poll_delete_option_a11y, answer.text),
modifier = Modifier.clickable(answer.canDelete) { modifier = Modifier.clickable(answer.canDelete) {
state.eventSink(CreatePollEvents.RemoveAnswer(index)) state.eventSink(CreatePollEvent.RemoveAnswer(index))
}, },
) )
}, },
@ -160,7 +161,7 @@ fun CreatePollView(
), ),
style = ListItemStyle.Primary, style = ListItemStyle.Primary,
onClick = { onClick = {
state.eventSink(CreatePollEvents.AddAnswer) state.eventSink(CreatePollEvent.AddAnswer)
coroutineScope.launch(Dispatchers.Main) { coroutineScope.launch(Dispatchers.Main) {
lazyListState.animateScrollToItem(state.answers.size + 1) lazyListState.animateScrollToItem(state.answers.size + 1)
answerFocusRequester.requestFocus() answerFocusRequester.requestFocus()
@ -180,7 +181,7 @@ fun CreatePollView(
), ),
onClick = { onClick = {
state.eventSink( state.eventSink(
CreatePollEvents.SetPollKind( CreatePollEvent.SetPollKind(
if (state.pollKind == PollKind.Disclosed) PollKind.Undisclosed else PollKind.Disclosed if (state.pollKind == PollKind.Disclosed) PollKind.Undisclosed else PollKind.Disclosed
) )
) )
@ -190,7 +191,7 @@ fun CreatePollView(
ListItem( ListItem(
headlineContent = { Text(text = stringResource(id = CommonStrings.action_delete_poll)) }, headlineContent = { Text(text = stringResource(id = CommonStrings.action_delete_poll)) },
style = ListItemStyle.Destructive, style = ListItemStyle.Destructive,
onClick = { state.eventSink(CreatePollEvents.Delete(confirmed = false)) }, onClick = { state.eventSink(CreatePollEvent.Delete(confirmed = false)) },
) )
} }
} }

View file

@ -104,15 +104,15 @@ class CreatePollPresenterTest {
val initial = awaitItem() val initial = awaitItem()
assertThat(initial.canSave).isFalse() assertThat(initial.canSave).isFalse()
initial.eventSink(CreatePollEvents.SetQuestion("A question?")) initial.eventSink(CreatePollEvent.SetQuestion("A question?"))
val questionSet = awaitItem() val questionSet = awaitItem()
assertThat(questionSet.canSave).isFalse() assertThat(questionSet.canSave).isFalse()
questionSet.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1")) questionSet.eventSink(CreatePollEvent.SetAnswer(0, "Answer 1"))
val answer1Set = awaitItem() val answer1Set = awaitItem()
assertThat(answer1Set.canSave).isFalse() assertThat(answer1Set.canSave).isFalse()
answer1Set.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2")) answer1Set.eventSink(CreatePollEvent.SetAnswer(1, "Answer 2"))
val answer2Set = awaitItem() val answer2Set = awaitItem()
assertThat(answer2Set.canSave).isTrue() assertThat(answer2Set.canSave).isTrue()
} }
@ -133,11 +133,11 @@ class CreatePollPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initial = awaitItem() val initial = awaitItem()
initial.eventSink(CreatePollEvents.SetQuestion("A question?")) initial.eventSink(CreatePollEvent.SetQuestion("A question?"))
initial.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1")) initial.eventSink(CreatePollEvent.SetAnswer(0, "Answer 1"))
initial.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2")) initial.eventSink(CreatePollEvent.SetAnswer(1, "Answer 2"))
skipItems(3) skipItems(3)
initial.eventSink(CreatePollEvents.Save) initial.eventSink(CreatePollEvent.Save)
delay(1) // Wait for the coroutine to finish delay(1) // Wait for the coroutine to finish
createPollResult.assertions().isCalledOnce() createPollResult.assertions().isCalledOnce()
.with( .with(
@ -182,10 +182,10 @@ class CreatePollPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
awaitDefaultItem().eventSink(CreatePollEvents.SetQuestion("A question?")) awaitDefaultItem().eventSink(CreatePollEvent.SetQuestion("A question?"))
awaitItem().eventSink(CreatePollEvents.SetAnswer(0, "Answer 1")) awaitItem().eventSink(CreatePollEvent.SetAnswer(0, "Answer 1"))
awaitItem().eventSink(CreatePollEvents.SetAnswer(1, "Answer 2")) awaitItem().eventSink(CreatePollEvent.SetAnswer(1, "Answer 2"))
awaitItem().eventSink(CreatePollEvents.Save) awaitItem().eventSink(CreatePollEvent.Save)
delay(1) // Wait for the coroutine to finish delay(1) // Wait for the coroutine to finish
createPollResult.assertions().isCalledOnce() createPollResult.assertions().isCalledOnce()
assertThat(fakeAnalyticsService.capturedEvents).isEmpty() assertThat(fakeAnalyticsService.capturedEvents).isEmpty()
@ -210,20 +210,20 @@ class CreatePollPresenterTest {
}.test { }.test {
awaitDefaultItem() awaitDefaultItem()
awaitPollLoaded().apply { awaitPollLoaded().apply {
eventSink(CreatePollEvents.SetQuestion("Changed question")) eventSink(CreatePollEvent.SetQuestion("Changed question"))
} }
awaitItem().apply { awaitItem().apply {
eventSink(CreatePollEvents.SetAnswer(0, "Changed answer 1")) eventSink(CreatePollEvent.SetAnswer(0, "Changed answer 1"))
} }
awaitItem().apply { awaitItem().apply {
eventSink(CreatePollEvents.SetAnswer(1, "Changed answer 2")) eventSink(CreatePollEvent.SetAnswer(1, "Changed answer 2"))
} }
awaitPollLoaded( awaitPollLoaded(
newQuestion = "Changed question", newQuestion = "Changed question",
newAnswer1 = "Changed answer 1", newAnswer1 = "Changed answer 1",
newAnswer2 = "Changed answer 2", newAnswer2 = "Changed answer 2",
).apply { ).apply {
eventSink(CreatePollEvents.Save) eventSink(CreatePollEvent.Save)
} }
advanceUntilIdle() // Wait for the coroutine to finish advanceUntilIdle() // Wait for the coroutine to finish
@ -275,8 +275,8 @@ class CreatePollPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
awaitDefaultItem() awaitDefaultItem()
awaitPollLoaded().eventSink(CreatePollEvents.SetAnswer(0, "A")) awaitPollLoaded().eventSink(CreatePollEvent.SetAnswer(0, "A"))
awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvents.Save) awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvent.Save)
advanceUntilIdle() // Wait for the coroutine to finish advanceUntilIdle() // Wait for the coroutine to finish
editPollLambda.assertions().isCalledOnce() editPollLambda.assertions().isCalledOnce()
assertThat(fakeAnalyticsService.capturedEvents).isEmpty() assertThat(fakeAnalyticsService.capturedEvents).isEmpty()
@ -296,12 +296,12 @@ class CreatePollPresenterTest {
val initial = awaitItem() val initial = awaitItem()
assertThat(initial.answers.size).isEqualTo(2) assertThat(initial.answers.size).isEqualTo(2)
initial.eventSink(CreatePollEvents.AddAnswer) initial.eventSink(CreatePollEvent.AddAnswer)
val answerAdded = awaitItem() val answerAdded = awaitItem()
assertThat(answerAdded.answers.size).isEqualTo(3) assertThat(answerAdded.answers.size).isEqualTo(3)
assertThat(answerAdded.answers[2].text).isEmpty() assertThat(answerAdded.answers[2].text).isEmpty()
initial.eventSink(CreatePollEvents.RemoveAnswer(2)) initial.eventSink(CreatePollEvent.RemoveAnswer(2))
val answerRemoved = awaitItem() val answerRemoved = awaitItem()
assertThat(answerRemoved.answers.size).isEqualTo(2) assertThat(answerRemoved.answers.size).isEqualTo(2)
} }
@ -314,7 +314,7 @@ class CreatePollPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initial = awaitItem() val initial = awaitItem()
initial.eventSink(CreatePollEvents.SetQuestion("A question?")) initial.eventSink(CreatePollEvent.SetQuestion("A question?"))
val questionSet = awaitItem() val questionSet = awaitItem()
assertThat(questionSet.question).isEqualTo("A question?") assertThat(questionSet.question).isEqualTo("A question?")
} }
@ -327,7 +327,7 @@ class CreatePollPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initial = awaitItem() val initial = awaitItem()
initial.eventSink(CreatePollEvents.SetAnswer(0, "This is answer 1")) initial.eventSink(CreatePollEvent.SetAnswer(0, "This is answer 1"))
val answerSet = awaitItem() val answerSet = awaitItem()
assertThat(answerSet.answers.first().text).isEqualTo("This is answer 1") assertThat(answerSet.answers.first().text).isEqualTo("This is answer 1")
} }
@ -340,7 +340,7 @@ class CreatePollPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initial = awaitItem() val initial = awaitItem()
initial.eventSink(CreatePollEvents.SetPollKind(PollKind.Undisclosed)) initial.eventSink(CreatePollEvent.SetPollKind(PollKind.Undisclosed))
val kindSet = awaitItem() val kindSet = awaitItem()
assertThat(kindSet.pollKind).isEqualTo(PollKind.Undisclosed) assertThat(kindSet.pollKind).isEqualTo(PollKind.Undisclosed)
} }
@ -355,10 +355,10 @@ class CreatePollPresenterTest {
val initial = awaitItem() val initial = awaitItem()
assertThat(initial.canAddAnswer).isTrue() assertThat(initial.canAddAnswer).isTrue()
repeat(17) { repeat(17) {
initial.eventSink(CreatePollEvents.AddAnswer) initial.eventSink(CreatePollEvent.AddAnswer)
assertThat(awaitItem().canAddAnswer).isTrue() assertThat(awaitItem().canAddAnswer).isTrue()
} }
initial.eventSink(CreatePollEvents.AddAnswer) initial.eventSink(CreatePollEvent.AddAnswer)
assertThat(awaitItem().canAddAnswer).isFalse() assertThat(awaitItem().canAddAnswer).isFalse()
} }
} }
@ -371,7 +371,7 @@ class CreatePollPresenterTest {
}.test { }.test {
val initial = awaitItem() val initial = awaitItem()
assertThat(initial.answers.all { it.canDelete }).isFalse() assertThat(initial.answers.all { it.canDelete }).isFalse()
initial.eventSink(CreatePollEvents.AddAnswer) initial.eventSink(CreatePollEvent.AddAnswer)
assertThat(awaitItem().answers.all { it.canDelete }).isTrue() assertThat(awaitItem().answers.all { it.canDelete }).isTrue()
} }
} }
@ -383,7 +383,7 @@ class CreatePollPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initial = awaitItem() 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) assertThat(awaitItem().answers.first().text.length).isEqualTo(240)
} }
} }
@ -396,7 +396,7 @@ class CreatePollPresenterTest {
}.test { }.test {
val initial = awaitItem() val initial = awaitItem()
assertThat(navUpInvocationsCount).isEqualTo(0) assertThat(navUpInvocationsCount).isEqualTo(0)
initial.eventSink(CreatePollEvents.NavBack) initial.eventSink(CreatePollEvent.NavBack)
assertThat(navUpInvocationsCount).isEqualTo(1) assertThat(navUpInvocationsCount).isEqualTo(1)
} }
} }
@ -410,7 +410,7 @@ class CreatePollPresenterTest {
val initial = awaitItem() val initial = awaitItem()
assertThat(navUpInvocationsCount).isEqualTo(0) assertThat(navUpInvocationsCount).isEqualTo(0)
assertThat(initial.showBackConfirmation).isFalse() assertThat(initial.showBackConfirmation).isFalse()
initial.eventSink(CreatePollEvents.ConfirmNavBack) initial.eventSink(CreatePollEvent.ConfirmNavBack)
assertThat(navUpInvocationsCount).isEqualTo(1) assertThat(navUpInvocationsCount).isEqualTo(1)
} }
} }
@ -422,11 +422,11 @@ class CreatePollPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initial = awaitItem() val initial = awaitItem()
initial.eventSink(CreatePollEvents.SetQuestion("Non blank")) initial.eventSink(CreatePollEvent.SetQuestion("Non blank"))
assertThat(awaitItem().showBackConfirmation).isFalse() assertThat(awaitItem().showBackConfirmation).isFalse()
initial.eventSink(CreatePollEvents.ConfirmNavBack) initial.eventSink(CreatePollEvent.ConfirmNavBack)
assertThat(awaitItem().showBackConfirmation).isTrue() assertThat(awaitItem().showBackConfirmation).isTrue()
initial.eventSink(CreatePollEvents.HideConfirmation) initial.eventSink(CreatePollEvent.HideConfirmation)
assertThat(awaitItem().showBackConfirmation).isFalse() assertThat(awaitItem().showBackConfirmation).isFalse()
assertThat(navUpInvocationsCount).isEqualTo(0) assertThat(navUpInvocationsCount).isEqualTo(0)
} }
@ -442,7 +442,7 @@ class CreatePollPresenterTest {
val loaded = awaitPollLoaded() val loaded = awaitPollLoaded()
assertThat(navUpInvocationsCount).isEqualTo(0) assertThat(navUpInvocationsCount).isEqualTo(0)
assertThat(loaded.showBackConfirmation).isFalse() assertThat(loaded.showBackConfirmation).isFalse()
loaded.eventSink(CreatePollEvents.ConfirmNavBack) loaded.eventSink(CreatePollEvent.ConfirmNavBack)
assertThat(navUpInvocationsCount).isEqualTo(1) assertThat(navUpInvocationsCount).isEqualTo(1)
} }
} }
@ -455,11 +455,11 @@ class CreatePollPresenterTest {
}.test { }.test {
awaitDefaultItem() awaitDefaultItem()
val loaded = awaitPollLoaded() val loaded = awaitPollLoaded()
loaded.eventSink(CreatePollEvents.SetQuestion("CHANGED")) loaded.eventSink(CreatePollEvent.SetQuestion("CHANGED"))
assertThat(awaitItem().showBackConfirmation).isFalse() assertThat(awaitItem().showBackConfirmation).isFalse()
loaded.eventSink(CreatePollEvents.ConfirmNavBack) loaded.eventSink(CreatePollEvent.ConfirmNavBack)
assertThat(awaitItem().showBackConfirmation).isTrue() assertThat(awaitItem().showBackConfirmation).isTrue()
loaded.eventSink(CreatePollEvents.HideConfirmation) loaded.eventSink(CreatePollEvent.HideConfirmation)
assertThat(awaitItem().showBackConfirmation).isFalse() assertThat(awaitItem().showBackConfirmation).isFalse()
assertThat(navUpInvocationsCount).isEqualTo(0) assertThat(navUpInvocationsCount).isEqualTo(0)
} }
@ -474,7 +474,7 @@ class CreatePollPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
awaitDefaultItem() awaitDefaultItem()
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false)) awaitPollLoaded().eventSink(CreatePollEvent.Delete(confirmed = false))
awaitDeleteConfirmation() awaitDeleteConfirmation()
assert(redactEventLambda).isNeverCalled() assert(redactEventLambda).isNeverCalled()
} }
@ -489,8 +489,8 @@ class CreatePollPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
awaitDefaultItem() awaitDefaultItem()
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false)) awaitPollLoaded().eventSink(CreatePollEvent.Delete(confirmed = false))
awaitDeleteConfirmation().eventSink(CreatePollEvents.HideConfirmation) awaitDeleteConfirmation().eventSink(CreatePollEvent.HideConfirmation)
awaitPollLoaded().apply { awaitPollLoaded().apply {
assertThat(showDeleteConfirmation).isFalse() assertThat(showDeleteConfirmation).isFalse()
} }
@ -507,8 +507,8 @@ class CreatePollPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
awaitDefaultItem() awaitDefaultItem()
awaitPollLoaded().eventSink(CreatePollEvents.Delete(confirmed = false)) awaitPollLoaded().eventSink(CreatePollEvent.Delete(confirmed = false))
awaitDeleteConfirmation().eventSink(CreatePollEvents.Delete(confirmed = true)) awaitDeleteConfirmation().eventSink(CreatePollEvent.Delete(confirmed = true))
awaitPollLoaded().apply { awaitPollLoaded().apply {
assertThat(showDeleteConfirmation).isFalse() assertThat(showDeleteConfirmation).isFalse()
} }

View file

@ -10,10 +10,10 @@ package io.element.android.features.preferences.impl.user.editprofile
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
sealed interface EditUserProfileEvents { sealed interface EditUserProfileEvent {
data class HandleAvatarAction(val action: AvatarAction) : EditUserProfileEvents data class HandleAvatarAction(val action: AvatarAction) : EditUserProfileEvent
data class UpdateDisplayName(val name: String) : EditUserProfileEvents data class UpdateDisplayName(val name: String) : EditUserProfileEvent
data object Exit : EditUserProfileEvents data object Exit : EditUserProfileEvent
data object Save : EditUserProfileEvents data object Save : EditUserProfileEvent
data object CloseDialog : EditUserProfileEvents data object CloseDialog : EditUserProfileEvent
} }

View file

@ -112,15 +112,15 @@ class EditUserProfilePresenter(
!userDisplayName.isNullOrBlank() && hasProfileChanged !userDisplayName.isNullOrBlank() && hasProfileChanged
} }
fun handleEvent(event: EditUserProfileEvents) { fun handleEvent(event: EditUserProfileEvent) {
when (event) { when (event) {
is EditUserProfileEvents.Save -> localCoroutineScope.saveChanges( is EditUserProfileEvent.Save -> localCoroutineScope.saveChanges(
name = userDisplayName, name = userDisplayName,
avatarUri = userAvatarUri?.toUri(), avatarUri = userAvatarUri?.toUri(),
currentUser = matrixUser, currentUser = matrixUser,
action = saveAction, action = saveAction,
) )
is EditUserProfileEvents.HandleAvatarAction -> { is EditUserProfileEvent.HandleAvatarAction -> {
when (event.action) { when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch() AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) { AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
@ -135,8 +135,8 @@ class EditUserProfilePresenter(
} }
} }
} }
is EditUserProfileEvents.UpdateDisplayName -> userDisplayName = event.name is EditUserProfileEvent.UpdateDisplayName -> userDisplayName = event.name
EditUserProfileEvents.Exit -> { EditUserProfileEvent.Exit -> {
when (saveAction.value) { when (saveAction.value) {
is AsyncAction.Confirming -> { is AsyncAction.Confirming -> {
// Close the dialog right now // Close the dialog right now
@ -157,7 +157,7 @@ class EditUserProfilePresenter(
} }
} }
} }
EditUserProfileEvents.CloseDialog -> saveAction.value = AsyncAction.Uninitialized EditUserProfileEvent.CloseDialog -> saveAction.value = AsyncAction.Uninitialized
} }
} }

View file

@ -22,5 +22,5 @@ data class EditUserProfileState(
val saveButtonEnabled: Boolean, val saveButtonEnabled: Boolean,
val saveAction: AsyncAction<Unit>, val saveAction: AsyncAction<Unit>,
val cameraPermissionState: PermissionsState, val cameraPermissionState: PermissionsState,
val eventSink: (EditUserProfileEvents) -> Unit val eventSink: (EditUserProfileEvent) -> Unit
) )

View file

@ -33,7 +33,7 @@ fun aEditUserProfileState(
saveButtonEnabled: Boolean = true, saveButtonEnabled: Boolean = true,
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized, saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false), cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
eventSink: (EditUserProfileEvents) -> Unit = {}, eventSink: (EditUserProfileEvent) -> Unit = {},
) = EditUserProfileState( ) = EditUserProfileState(
userId = userId, userId = userId,
displayName = displayName, displayName = displayName,

View file

@ -68,7 +68,7 @@ fun EditUserProfileView(
fun onBackClick() { fun onBackClick() {
focusManager.clearFocus() focusManager.clearFocus()
state.eventSink(EditUserProfileEvents.Exit) state.eventSink(EditUserProfileEvent.Exit)
} }
BackHandler( BackHandler(
@ -87,7 +87,7 @@ fun EditUserProfileView(
enabled = state.saveButtonEnabled, enabled = state.saveButtonEnabled,
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
state.eventSink(EditUserProfileEvents.Save) state.eventSink(EditUserProfileEvent.Save)
}, },
) )
} }
@ -125,7 +125,7 @@ fun EditUserProfileView(
value = state.displayName, value = state.displayName,
placeholder = stringResource(CommonStrings.common_room_name_placeholder), placeholder = stringResource(CommonStrings.common_room_name_placeholder),
singleLine = true, singleLine = true,
onValueChange = { state.eventSink(EditUserProfileEvents.UpdateDisplayName(it)) }, onValueChange = { state.eventSink(EditUserProfileEvent.UpdateDisplayName(it)) },
) )
} }
@ -133,7 +133,7 @@ fun EditUserProfileView(
actions = state.avatarActions, actions = state.avatarActions,
isVisible = isAvatarActionsSheetVisible.value, isVisible = isAvatarActionsSheetVisible.value,
onDismiss = { isAvatarActionsSheetVisible.value = false }, onDismiss = { isAvatarActionsSheetVisible.value = false },
onSelectAction = { state.eventSink(EditUserProfileEvents.HandleAvatarAction(it)) } onSelectAction = { state.eventSink(EditUserProfileEvent.HandleAvatarAction(it)) }
) )
AsyncActionView( AsyncActionView(
@ -147,8 +147,9 @@ fun EditUserProfileView(
when (confirming) { when (confirming) {
is AsyncAction.ConfirmingCancellation -> { is AsyncAction.ConfirmingCancellation -> {
SaveChangesDialog( SaveChangesDialog(
onSubmitClick = { state.eventSink(EditUserProfileEvents.Exit) }, onSaveClick = { state.eventSink(EditUserProfileEvent.Save) },
onDismiss = { state.eventSink(EditUserProfileEvents.CloseDialog) } onDiscardClick = { state.eventSink(EditUserProfileEvent.Exit) },
onDismiss = { state.eventSink(EditUserProfileEvent.CloseDialog) },
) )
} }
} }
@ -156,7 +157,7 @@ fun EditUserProfileView(
onSuccess = { onEditProfileSuccess() }, onSuccess = { onEditProfileSuccess() },
errorTitle = { stringResource(R.string.screen_edit_profile_error_title) }, errorTitle = { stringResource(R.string.screen_edit_profile_error_title) },
errorMessage = { stringResource(R.string.screen_edit_profile_error) }, errorMessage = { stringResource(R.string.screen_edit_profile_error) },
onErrorDismiss = { state.eventSink(EditUserProfileEvents.CloseDialog) }, onErrorDismiss = { state.eventSink(EditUserProfileEvent.CloseDialog) },
) )
} }
PermissionsView( PermissionsView(

View file

@ -124,7 +124,7 @@ class EditUserProfilePresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.Exit) initialState.eventSink(EditUserProfileEvent.Exit)
closeLambda.assertions().isCalledOnce() closeLambda.assertions().isCalledOnce()
} }
} }
@ -139,21 +139,21 @@ class EditUserProfilePresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("New name")) initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("New name"))
val withUpdatedName = awaitItem() val withUpdatedName = awaitItem()
withUpdatedName.eventSink(EditUserProfileEvents.Exit) withUpdatedName.eventSink(EditUserProfileEvent.Exit)
val withConfirmation = awaitItem() val withConfirmation = awaitItem()
assertThat(withConfirmation.saveAction).isEqualTo(AsyncAction.ConfirmingCancellation) assertThat(withConfirmation.saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
// Cancel // Cancel
withConfirmation.eventSink(EditUserProfileEvents.CloseDialog) withConfirmation.eventSink(EditUserProfileEvent.CloseDialog)
val afterCancel = awaitItem() val afterCancel = awaitItem()
assertThat(afterCancel.saveAction).isEqualTo(AsyncAction.Uninitialized) assertThat(afterCancel.saveAction).isEqualTo(AsyncAction.Uninitialized)
// Try again and confirm // Try again and confirm
afterCancel.eventSink(EditUserProfileEvents.Exit) afterCancel.eventSink(EditUserProfileEvent.Exit)
val withConfirmation2 = awaitItem() val withConfirmation2 = awaitItem()
assertThat(withConfirmation2.saveAction).isEqualTo(AsyncAction.ConfirmingCancellation) assertThat(withConfirmation2.saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
closeLambda.assertions().isNeverCalled() closeLambda.assertions().isNeverCalled()
withConfirmation2.eventSink(EditUserProfileEvents.Exit) withConfirmation2.eventSink(EditUserProfileEvent.Exit)
// Dialog is closed // Dialog is closed
val finalState = awaitItem() val finalState = awaitItem()
assertThat(finalState.saveAction).isEqualTo(AsyncAction.Uninitialized) assertThat(finalState.saveAction).isEqualTo(AsyncAction.Uninitialized)
@ -174,17 +174,17 @@ class EditUserProfilePresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.displayName).isEqualTo("Name") assertThat(initialState.displayName).isEqualTo("Name")
assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name II"))
awaitItem().apply { awaitItem().apply {
assertThat(displayName).isEqualTo("Name II") assertThat(displayName).isEqualTo("Name II")
assertThat(userAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(userAvatarUrl).isEqualTo(AN_AVATAR_URL)
} }
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name III")) initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name III"))
awaitItem().apply { awaitItem().apply {
assertThat(displayName).isEqualTo("Name III") assertThat(displayName).isEqualTo("Name III")
assertThat(userAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(userAvatarUrl).isEqualTo(AN_AVATAR_URL)
} }
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply { awaitItem().apply {
assertThat(displayName).isEqualTo("Name III") assertThat(displayName).isEqualTo("Name III")
assertThat(userAvatarUrl).isNull() assertThat(userAvatarUrl).isNull()
@ -205,7 +205,7 @@ class EditUserProfilePresenterTest {
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply { awaitItem().apply {
assertThat(userAvatarUrl).isEqualTo(ANOTHER_AVATAR_URL) assertThat(userAvatarUrl).isEqualTo(ANOTHER_AVATAR_URL)
} }
@ -229,7 +229,7 @@ class EditUserProfilePresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(initialState.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
assertThat(initialState.cameraPermissionState.permissionGranted).isFalse() assertThat(initialState.cameraPermissionState.permissionGranted).isFalse()
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto)) initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.TakePhoto))
val stateWithAskingPermission = awaitItem() val stateWithAskingPermission = awaitItem()
assertThat(stateWithAskingPermission.cameraPermissionState.showDialog).isTrue() assertThat(stateWithAskingPermission.cameraPermissionState.showDialog).isTrue()
fakePermissionsPresenter.setPermissionGranted() fakePermissionsPresenter.setPermissionGranted()
@ -239,7 +239,7 @@ class EditUserProfilePresenterTest {
assertThat(stateWithNewAvatar.userAvatarUrl).isEqualTo(ANOTHER_AVATAR_URL) assertThat(stateWithNewAvatar.userAvatarUrl).isEqualTo(ANOTHER_AVATAR_URL)
// Do it again, no permission is requested // Do it again, no permission is requested
fakePickerProvider.givenResult(userAvatarUri) fakePickerProvider.givenResult(userAvatarUri)
stateWithNewAvatar.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.TakePhoto)) stateWithNewAvatar.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.TakePhoto))
val stateWithNewAvatar2 = awaitItem() val stateWithNewAvatar2 = awaitItem()
assertThat(stateWithNewAvatar2.userAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(stateWithNewAvatar2.userAvatarUrl).isEqualTo(AN_AVATAR_URL)
deleteCallback.assertions().isCalledExactly(2).withSequence( deleteCallback.assertions().isCalledExactly(2).withSequence(
@ -264,22 +264,22 @@ class EditUserProfilePresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.saveButtonEnabled).isFalse() assertThat(initialState.saveButtonEnabled).isFalse()
// Once a change is made, the save button is enabled // Once a change is made, the save button is enabled
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name II"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
} }
// If it's reverted then the save disables again // If it's reverted then the save disables again
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name")) initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isFalse() assertThat(saveButtonEnabled).isFalse()
} }
// Make a change... // Make a change...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
} }
// Revert it... // Revert it...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isFalse() assertThat(saveButtonEnabled).isFalse()
} }
@ -305,22 +305,22 @@ class EditUserProfilePresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.saveButtonEnabled).isFalse() assertThat(initialState.saveButtonEnabled).isFalse()
// Once a change is made, the save button is enabled // Once a change is made, the save button is enabled
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name II")) initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name II"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
} }
// If it's reverted then the save disables again // If it's reverted then the save disables again
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("Name")) initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("Name"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isFalse() assertThat(saveButtonEnabled).isFalse()
} }
// Make a change... // Make a change...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
} }
// Revert it... // Revert it...
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isFalse() assertThat(saveButtonEnabled).isFalse()
} }
@ -344,9 +344,9 @@ class EditUserProfilePresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("New name")) initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("New name"))
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.Remove))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvent.Save)
consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled } consumeItemsUntilPredicate { matrixClient.setDisplayNameCalled && matrixClient.removeAvatarCalled && !matrixClient.uploadAvatarCalled }
assertThat(matrixClient.setDisplayNameCalled).isTrue() assertThat(matrixClient.setDisplayNameCalled).isTrue()
assertThat(matrixClient.removeAvatarCalled).isTrue() assertThat(matrixClient.removeAvatarCalled).isTrue()
@ -365,8 +365,8 @@ class EditUserProfilePresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName(" Name ")) initialState.eventSink(EditUserProfileEvent.UpdateDisplayName(" Name "))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvent.Save)
consumeItemsUntilTimeout() consumeItemsUntilTimeout()
assertThat(matrixClient.setDisplayNameCalled).isFalse() assertThat(matrixClient.setDisplayNameCalled).isFalse()
assertThat(matrixClient.uploadAvatarCalled).isFalse() assertThat(matrixClient.uploadAvatarCalled).isFalse()
@ -384,8 +384,8 @@ class EditUserProfilePresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("")) initialState.eventSink(EditUserProfileEvent.UpdateDisplayName(""))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvent.Save)
assertThat(matrixClient.setDisplayNameCalled).isFalse() assertThat(matrixClient.setDisplayNameCalled).isFalse()
assertThat(matrixClient.uploadAvatarCalled).isFalse() assertThat(matrixClient.uploadAvatarCalled).isFalse()
assertThat(matrixClient.removeAvatarCalled).isFalse() assertThat(matrixClient.removeAvatarCalled).isFalse()
@ -407,8 +407,8 @@ class EditUserProfilePresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvent.Save)
consumeItemsUntilPredicate { matrixClient.uploadAvatarCalled } consumeItemsUntilPredicate { matrixClient.uploadAvatarCalled }
assertThat(matrixClient.uploadAvatarCalled).isTrue() assertThat(matrixClient.uploadAvatarCalled).isTrue()
} }
@ -429,8 +429,8 @@ class EditUserProfilePresenterTest {
fakeMediaPreProcessor.givenResult(Result.failure(RuntimeException("Oh no"))) fakeMediaPreProcessor.givenResult(Result.failure(RuntimeException("Oh no")))
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvent.Save)
skipItems(2) skipItems(2)
assertThat(matrixClient.uploadAvatarCalled).isFalse() assertThat(matrixClient.uploadAvatarCalled).isFalse()
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
@ -443,7 +443,7 @@ class EditUserProfilePresenterTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
givenSetDisplayNameResult(Result.failure(RuntimeException("!"))) givenSetDisplayNameResult(Result.failure(RuntimeException("!")))
} }
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.UpdateDisplayName("New name")) saveAndAssertFailure(user, matrixClient, EditUserProfileEvent.UpdateDisplayName("New name"))
} }
@Test @Test
@ -452,7 +452,7 @@ class EditUserProfilePresenterTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
givenRemoveAvatarResult(Result.failure(RuntimeException("!"))) givenRemoveAvatarResult(Result.failure(RuntimeException("!")))
} }
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.Remove)) saveAndAssertFailure(user, matrixClient, EditUserProfileEvent.HandleAvatarAction(AvatarAction.Remove))
} }
@Test @Test
@ -462,7 +462,7 @@ class EditUserProfilePresenterTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
givenUploadAvatarResult(Result.failure(RuntimeException("!"))) givenUploadAvatarResult(Result.failure(RuntimeException("!")))
} }
saveAndAssertFailure(user, matrixClient, EditUserProfileEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) saveAndAssertFailure(user, matrixClient, EditUserProfileEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
} }
@Test @Test
@ -475,16 +475,16 @@ class EditUserProfilePresenterTest {
val presenter = createEditUserProfilePresenter(matrixUser = user, matrixClient = matrixClient) val presenter = createEditUserProfilePresenter(matrixUser = user, matrixClient = matrixClient)
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(EditUserProfileEvents.UpdateDisplayName("foo")) initialState.eventSink(EditUserProfileEvent.UpdateDisplayName("foo"))
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvent.Save)
skipItems(2) skipItems(2)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
initialState.eventSink(EditUserProfileEvents.CloseDialog) initialState.eventSink(EditUserProfileEvent.CloseDialog)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Uninitialized::class.java) 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( val presenter = createEditUserProfilePresenter(
matrixUser = matrixUser, matrixUser = matrixUser,
matrixClient = matrixClient, matrixClient = matrixClient,
@ -495,7 +495,7 @@ class EditUserProfilePresenterTest {
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(event) initialState.eventSink(event)
initialState.eventSink(EditUserProfileEvents.Save) initialState.eventSink(EditUserProfileEvent.Save)
skipItems(1) skipItems(1)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)

View file

@ -34,45 +34,45 @@ class EditUserProfileViewTest {
@Test @Test
fun `clicking on back emits the expected event`() { fun `clicking on back emits the expected event`() {
val eventsRecorder = EventsRecorder<EditUserProfileEvents>() val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
rule.setEditUserProfileView( rule.setEditUserProfileView(
aEditUserProfileState( aEditUserProfileState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.pressBack() rule.pressBack()
eventsRecorder.assertSingle(EditUserProfileEvents.Exit) eventsRecorder.assertSingle(EditUserProfileEvent.Exit)
} }
@Test @Test
fun `clicking on cancel exit emits the expected event`() { fun `clicking on save from the exit confirmation dialog emits the expected event`() {
val eventsRecorder = EventsRecorder<EditUserProfileEvents>() val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
rule.setEditUserProfileView( rule.setEditUserProfileView(
aEditUserProfileState( aEditUserProfileState(
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_cancel) rule.clickOn(CommonStrings.action_save, inDialog = true)
eventsRecorder.assertSingle(EditUserProfileEvents.CloseDialog) eventsRecorder.assertSingle(EditUserProfileEvent.Save)
} }
@Test @Test
fun `clicking on OK exit emits the expected event`() { fun `clicking on discard exit emits the expected event`() {
val eventsRecorder = EventsRecorder<EditUserProfileEvents>() val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
rule.setEditUserProfileView( rule.setEditUserProfileView(
aEditUserProfileState( aEditUserProfileState(
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_ok) rule.clickOn(CommonStrings.action_discard)
eventsRecorder.assertSingle(EditUserProfileEvents.Exit) eventsRecorder.assertSingle(EditUserProfileEvent.Exit)
} }
@Test @Test
fun `clicking on save emits the expected event`() { fun `clicking on save emits the expected event`() {
val eventsRecorder = EventsRecorder<EditUserProfileEvents>() val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
rule.setEditUserProfileView( rule.setEditUserProfileView(
aEditUserProfileState( aEditUserProfileState(
saveButtonEnabled = true, saveButtonEnabled = true,
@ -81,12 +81,12 @@ class EditUserProfileViewTest {
), ),
) )
rule.clickOn(CommonStrings.action_save) rule.clickOn(CommonStrings.action_save)
eventsRecorder.assertSingle(EditUserProfileEvents.Save) eventsRecorder.assertSingle(EditUserProfileEvent.Save)
} }
@Test @Test
fun `clicking on avatar opens the bottom sheet dialog`() { fun `clicking on avatar opens the bottom sheet dialog`() {
val eventsRecorder = EventsRecorder<EditUserProfileEvents>() val eventsRecorder = EventsRecorder<EditUserProfileEvent>()
val actions = listOf( val actions = listOf(
AvatarAction.TakePhoto, AvatarAction.TakePhoto,
AvatarAction.ChoosePhoto, AvatarAction.ChoosePhoto,
@ -110,7 +110,7 @@ class EditUserProfileViewTest {
@Test @Test
fun `success invokes the expected callback`() { fun `success invokes the expected callback`() {
val eventsRecorder = EventsRecorder<EditUserProfileEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<EditUserProfileEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setEditUserProfileView( rule.setEditUserProfileView(
aEditUserProfileState( aEditUserProfileState(

View file

@ -73,8 +73,7 @@ class ChangeRoomPermissionsPresenter(
private var initialPermissions by mutableStateOf<RoomPowerLevelsValues?>(null) private var initialPermissions by mutableStateOf<RoomPowerLevelsValues?>(null)
private var currentPermissions by mutableStateOf<RoomPowerLevelsValues?>(null) private var currentPermissions by mutableStateOf<RoomPowerLevelsValues?>(null)
private var saveAction by mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) private var saveAction by mutableStateOf<AsyncAction<Boolean>>(AsyncAction.Uninitialized)
private var confirmExitAction by mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
@Composable @Composable
override fun present(): ChangeRoomPermissionsState { override fun present(): ChangeRoomPermissionsState {
@ -109,15 +108,14 @@ class ChangeRoomPermissionsPresenter(
} }
is ChangeRoomPermissionsEvent.Save -> coroutineScope.save() is ChangeRoomPermissionsEvent.Save -> coroutineScope.save()
is ChangeRoomPermissionsEvent.Exit -> { is ChangeRoomPermissionsEvent.Exit -> {
confirmExitAction = if (!hasChanges || confirmExitAction.isConfirming()) { saveAction = if (!hasChanges || saveAction == AsyncAction.ConfirmingCancellation) {
AsyncAction.Success(Unit) AsyncAction.Success(false)
} else { } else {
AsyncAction.ConfirmingNoParams AsyncAction.ConfirmingCancellation
} }
} }
is ChangeRoomPermissionsEvent.ResetPendingActions -> { is ChangeRoomPermissionsEvent.ResetPendingActions -> {
saveAction = AsyncAction.Uninitialized saveAction = AsyncAction.Uninitialized
confirmExitAction = AsyncAction.Uninitialized
} }
} }
} }
@ -126,7 +124,6 @@ class ChangeRoomPermissionsPresenter(
itemsBySection = itemsBySection, itemsBySection = itemsBySection,
hasChanges = hasChanges, hasChanges = hasChanges,
saveAction = saveAction, saveAction = saveAction,
confirmExitAction = confirmExitAction,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
@ -147,7 +144,7 @@ class ChangeRoomPermissionsPresenter(
.onSuccess { .onSuccess {
analyticsService.trackPermissionChangeAnalytics(initialPermissions, updatedRoomPowerLevels) analyticsService.trackPermissionChangeAnalytics(initialPermissions, updatedRoomPowerLevels)
initialPermissions = currentPermissions initialPermissions = currentPermissions
saveAction = AsyncAction.Success(Unit) saveAction = AsyncAction.Success(true)
} }
.onFailure { .onFailure {
saveAction = AsyncAction.Failure(it) saveAction = AsyncAction.Failure(it)

View file

@ -23,8 +23,7 @@ data class ChangeRoomPermissionsState(
val currentPermissions: RoomPowerLevelsValues?, val currentPermissions: RoomPowerLevelsValues?,
val itemsBySection: ImmutableMap<RoomPermissionsSection, ImmutableList<RoomPermissionType>>, val itemsBySection: ImmutableMap<RoomPermissionsSection, ImmutableList<RoomPermissionType>>,
val hasChanges: Boolean, val hasChanges: Boolean,
val saveAction: AsyncAction<Unit>, val saveAction: AsyncAction<Boolean>,
val confirmExitAction: AsyncAction<Unit>,
val eventSink: (ChangeRoomPermissionsEvent) -> Unit, val eventSink: (ChangeRoomPermissionsEvent) -> Unit,
) { ) {
fun selectedRoleForType(type: RoomPermissionType): SelectableRole? { fun selectedRoleForType(type: RoomPermissionType): SelectableRole? {

View file

@ -25,7 +25,7 @@ class ChangeRoomPermissionsStateProvider : PreviewParameterProvider<ChangeRoomPe
hasChanges = true, hasChanges = true,
saveAction = AsyncAction.Failure(IllegalStateException("Failed to save changes")) 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(), currentPermissions: RoomPowerLevelsValues = previewPermissions(),
itemsBySection: Map<RoomPermissionsSection, ImmutableList<RoomPermissionType>> = ChangeRoomPermissionsPresenter.buildItems(false), itemsBySection: Map<RoomPermissionsSection, ImmutableList<RoomPermissionType>> = ChangeRoomPermissionsPresenter.buildItems(false),
hasChanges: Boolean = false, hasChanges: Boolean = false,
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized, saveAction: AsyncAction<Boolean> = AsyncAction.Uninitialized,
confirmExitAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (ChangeRoomPermissionsEvent) -> Unit = {}, eventSink: (ChangeRoomPermissionsEvent) -> Unit = {},
) = ChangeRoomPermissionsState( ) = ChangeRoomPermissionsState(
currentPermissions = currentPermissions, currentPermissions = currentPermissions,
itemsBySection = itemsBySection.toImmutableMap(), itemsBySection = itemsBySection.toImmutableMap(),
hasChanges = hasChanges, hasChanges = hasChanges,
saveAction = saveAction, saveAction = saveAction,
confirmExitAction = confirmExitAction,
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -18,9 +18,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.rolesandpermissions.impl.R 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.async.AsyncActionView
import io.element.android.libraries.designsystem.components.button.BackButton 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.components.preferences.PreferenceDropdown
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -91,24 +92,19 @@ fun ChangeRoomPermissionsView(
AsyncActionView( AsyncActionView(
async = state.saveAction, async = state.saveAction,
onSuccess = { onComplete(true) }, onSuccess = { onComplete(it) },
onErrorDismiss = { state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions) } confirmationDialog = { confirming ->
) when (confirming) {
is AsyncAction.ConfirmingCancellation -> {
AsyncActionView( SaveChangesDialog(
async = state.confirmExitAction, onSaveClick = { state.eventSink(ChangeRoomPermissionsEvent.Save) },
onSuccess = { onComplete(false) }, onDiscardClick = { state.eventSink(ChangeRoomPermissionsEvent.Exit) },
confirmationDialog = { onDismiss = { state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions) },
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) }
)
}, },
onErrorDismiss = {}, onErrorDismiss = { state.eventSink(ChangeRoomPermissionsEvent.ResetPendingActions) }
) )
} }

View file

@ -172,8 +172,9 @@ fun ChangeRolesView(
when (confirming) { when (confirming) {
is AsyncAction.ConfirmingCancellation -> { is AsyncAction.ConfirmingCancellation -> {
SaveChangesDialog( SaveChangesDialog(
onSubmitClick = { state.eventSink(ChangeRolesEvent.Exit) }, onSaveClick = { state.eventSink(ChangeRolesEvent.Save) },
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) } onDiscardClick = { state.eventSink(ChangeRolesEvent.Exit) },
onDismiss = { state.eventSink(ChangeRolesEvent.CloseDialog) },
) )
} }
is ConfirmingModifyingOwners -> { is ConfirmingModifyingOwners -> {

View file

@ -39,7 +39,6 @@ class ChangeRoomPermissionsPresenterTest {
assertThat(this.itemsBySection).isNotEmpty() assertThat(this.itemsBySection).isNotEmpty()
assertThat(this.hasChanges).isFalse() assertThat(this.hasChanges).isFalse()
assertThat(this.saveAction).isEqualTo(AsyncAction.Uninitialized) assertThat(this.saveAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(this.confirmExitAction).isEqualTo(AsyncAction.Uninitialized)
} }
// Updated state, permissions loaded // Updated state, permissions loaded
@ -162,7 +161,7 @@ class ChangeRoomPermissionsPresenterTest {
assertThat(awaitItem().hasChanges).isFalse() assertThat(awaitItem().hasChanges).isFalse()
awaitItem().run { awaitItem().run {
assertThat(currentPermissions?.roomName).isEqualTo(Moderator.powerLevel) assertThat(currentPermissions?.roomName).isEqualTo(Moderator.powerLevel)
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) assertThat(saveAction).isEqualTo(AsyncAction.Success(true))
} }
assertThat(analyticsService.capturedEvents).containsExactlyElementsIn( assertThat(analyticsService.capturedEvents).containsExactlyElementsIn(
listOf( listOf(
@ -243,10 +242,10 @@ class ChangeRoomPermissionsPresenterTest {
assertThat(awaitItem().hasChanges).isTrue() assertThat(awaitItem().hasChanges).isTrue()
state.eventSink(ChangeRoomPermissionsEvent.Exit) state.eventSink(ChangeRoomPermissionsEvent.Exit)
assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.ConfirmingNoParams) assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
state.eventSink(ChangeRoomPermissionsEvent.Exit) 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) state.eventSink(ChangeRoomPermissionsEvent.Exit)
assertThat(awaitItem().confirmExitAction).isEqualTo(AsyncAction.Success(Unit)) assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Success(false))
} }
} }

View file

@ -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.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn 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.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.pressBackKey import io.element.android.tests.testutils.pressBackKey
@ -76,7 +75,7 @@ class ChangeRoomPermissionsViewTest {
rule.setChangeRoomPermissionsRule( rule.setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
hasChanges = true, hasChanges = true,
confirmExitAction = AsyncAction.ConfirmingNoParams, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = recorder, eventSink = recorder,
), ),
) )
@ -90,11 +89,11 @@ class ChangeRoomPermissionsViewTest {
rule.setChangeRoomPermissionsRule( rule.setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
hasChanges = true, hasChanges = true,
confirmExitAction = AsyncAction.ConfirmingNoParams, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = recorder, eventSink = recorder,
), ),
) )
rule.clickOnFirst(CommonStrings.action_save) rule.clickOn(CommonStrings.action_save, inDialog = true)
recorder.assertSingle(ChangeRoomPermissionsEvent.Save) recorder.assertSingle(ChangeRoomPermissionsEvent.Save)
} }
@ -136,9 +135,23 @@ class ChangeRoomPermissionsViewTest {
rule.setChangeRoomPermissionsRule( rule.setChangeRoomPermissionsRule(
state = aChangeRoomPermissionsState( state = aChangeRoomPermissionsState(
hasChanges = true, 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) rule.clickOn(CommonStrings.action_save)
} }

View file

@ -119,7 +119,7 @@ class ChangeRolesViewTest {
} }
@Test @Test
fun `exit confirmation dialog - submit exits the screen`() { fun `exit confirmation dialog - discard exits the screen`() {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( rule.setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
@ -128,12 +128,12 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_ok) rule.clickOn(CommonStrings.action_discard)
eventsRecorder.assertSingle(ChangeRolesEvent.Exit) eventsRecorder.assertSingle(ChangeRolesEvent.Exit)
} }
@Test @Test
fun `exit confirmation dialog - cancel removes the dialog`() { fun `exit confirmation dialog - save emits the save event`() {
val eventsRecorder = EventsRecorder<ChangeRolesEvent>() val eventsRecorder = EventsRecorder<ChangeRolesEvent>()
rule.setChangeRolesContent( rule.setChangeRolesContent(
state = aChangeRolesState( state = aChangeRolesState(
@ -142,8 +142,8 @@ class ChangeRolesViewTest {
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_cancel) rule.clickOn(CommonStrings.action_save)
eventsRecorder.assertSingle(ChangeRolesEvent.CloseDialog) eventsRecorder.assertSingle(ChangeRolesEvent.Save)
} }
@Test @Test

View file

@ -383,7 +383,16 @@ class RoomDetailsFlowNode(
knockRequestsListEntryPoint.createNode(this, buildContext) knockRequestsListEntryPoint.createNode(this, buildContext)
} }
NavTarget.SecurityAndPrivacy -> { 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 -> { is NavTarget.VerifyUser -> {
val params = OutgoingVerificationEntryPoint.Params( val params = OutgoingVerificationEntryPoint.Params(

View file

@ -10,11 +10,11 @@ package io.element.android.features.roomdetailsedit.impl
import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.matrix.ui.media.AvatarAction
sealed interface RoomDetailsEditEvents { sealed interface RoomDetailsEditEvent {
data class HandleAvatarAction(val action: AvatarAction) : RoomDetailsEditEvents data class HandleAvatarAction(val action: AvatarAction) : RoomDetailsEditEvent
data class UpdateRoomName(val name: String) : RoomDetailsEditEvents data class UpdateRoomName(val name: String) : RoomDetailsEditEvent
data class UpdateRoomTopic(val topic: String) : RoomDetailsEditEvents data class UpdateRoomTopic(val topic: String) : RoomDetailsEditEvent
data object OnBackPress : RoomDetailsEditEvents data object OnBackPress : RoomDetailsEditEvent
data object Save : RoomDetailsEditEvents data object Save : RoomDetailsEditEvent
data object CloseDialog : RoomDetailsEditEvents data object CloseDialog : RoomDetailsEditEvent
} }

View file

@ -139,9 +139,9 @@ class RoomDetailsEditPresenter(
val saveAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val saveAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
fun handleEvent(event: RoomDetailsEditEvents) { fun handleEvent(event: RoomDetailsEditEvent) {
when (event) { when (event) {
is RoomDetailsEditEvents.Save -> localCoroutineScope.saveChanges( is RoomDetailsEditEvent.Save -> localCoroutineScope.saveChanges(
currentNameTrimmed = roomRawNameTrimmed, currentNameTrimmed = roomRawNameTrimmed,
newNameTrimmed = roomRawNameEdited.trim(), newNameTrimmed = roomRawNameEdited.trim(),
currentTopicTrimmed = roomTopicTrimmed, currentTopicTrimmed = roomTopicTrimmed,
@ -150,7 +150,7 @@ class RoomDetailsEditPresenter(
newAvatarUri = roomAvatarUriEdited?.toUri(), newAvatarUri = roomAvatarUriEdited?.toUri(),
action = saveAction, action = saveAction,
) )
is RoomDetailsEditEvents.HandleAvatarAction -> { is RoomDetailsEditEvent.HandleAvatarAction -> {
when (event.action) { when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch() AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) { AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
@ -166,10 +166,10 @@ class RoomDetailsEditPresenter(
} }
} }
is RoomDetailsEditEvents.UpdateRoomName -> roomRawNameEdited = event.name is RoomDetailsEditEvent.UpdateRoomName -> roomRawNameEdited = event.name
is RoomDetailsEditEvents.UpdateRoomTopic -> roomTopicEdited = event.topic is RoomDetailsEditEvent.UpdateRoomTopic -> roomTopicEdited = event.topic
RoomDetailsEditEvents.CloseDialog -> saveAction.value = AsyncAction.Uninitialized RoomDetailsEditEvent.CloseDialog -> saveAction.value = AsyncAction.Uninitialized
RoomDetailsEditEvents.OnBackPress -> if (saveButtonEnabled.not() || saveAction.value == AsyncAction.ConfirmingCancellation) { RoomDetailsEditEvent.OnBackPress -> if (saveButtonEnabled.not() || saveAction.value == AsyncAction.ConfirmingCancellation) {
// No changes to save or already confirming exit without saving // No changes to save or already confirming exit without saving
saveAction.value = AsyncAction.Success(Unit) saveAction.value = AsyncAction.Success(Unit)
} else { } else {

View file

@ -28,5 +28,5 @@ data class RoomDetailsEditState(
val saveAction: AsyncAction<Unit>, val saveAction: AsyncAction<Unit>,
val cameraPermissionState: PermissionsState, val cameraPermissionState: PermissionsState,
val isSpace: Boolean, val isSpace: Boolean,
val eventSink: (RoomDetailsEditEvents) -> Unit val eventSink: (RoomDetailsEditEvent) -> Unit
) )

View file

@ -45,7 +45,7 @@ fun aRoomDetailsEditState(
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized, saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false), cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
isSpace: Boolean = false, isSpace: Boolean = false,
eventSink: (RoomDetailsEditEvents) -> Unit = {}, eventSink: (RoomDetailsEditEvent) -> Unit = {},
) = RoomDetailsEditState( ) = RoomDetailsEditState(
roomId = roomId, roomId = roomId,
roomRawName = roomRawName, roomRawName = roomRawName,

View file

@ -64,7 +64,7 @@ fun RoomDetailsEditView(
} }
BackHandler { BackHandler {
state.eventSink(RoomDetailsEditEvents.OnBackPress) state.eventSink(RoomDetailsEditEvent.OnBackPress)
} }
Scaffold( Scaffold(
modifier = modifier.clearFocusOnTap(focusManager), modifier = modifier.clearFocusOnTap(focusManager),
@ -74,7 +74,7 @@ fun RoomDetailsEditView(
navigationIcon = { navigationIcon = {
BackButton( BackButton(
onClick = { onClick = {
state.eventSink(RoomDetailsEditEvents.OnBackPress) state.eventSink(RoomDetailsEditEvent.OnBackPress)
} }
) )
}, },
@ -84,7 +84,7 @@ fun RoomDetailsEditView(
enabled = state.saveButtonEnabled, enabled = state.saveButtonEnabled,
onClick = { onClick = {
focusManager.clearFocus() focusManager.clearFocus()
state.eventSink(RoomDetailsEditEvents.Save) state.eventSink(RoomDetailsEditEvent.Save)
}, },
) )
} }
@ -121,7 +121,7 @@ fun RoomDetailsEditView(
placeholder = stringResource(CommonStrings.common_room_name_placeholder), placeholder = stringResource(CommonStrings.common_room_name_placeholder),
singleLine = true, singleLine = true,
readOnly = !state.canChangeName, readOnly = !state.canChangeName,
onValueChange = { state.eventSink(RoomDetailsEditEvents.UpdateRoomName(it)) }, onValueChange = { state.eventSink(RoomDetailsEditEvent.UpdateRoomName(it)) },
) )
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
@ -136,7 +136,7 @@ fun RoomDetailsEditView(
}, },
maxLines = 10, maxLines = 10,
readOnly = !state.canChangeTopic, readOnly = !state.canChangeTopic,
onValueChange = { state.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(it)) }, onValueChange = { state.eventSink(RoomDetailsEditEvent.UpdateRoomTopic(it)) },
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences, capitalization = KeyboardCapitalization.Sentences,
), ),
@ -147,7 +147,7 @@ fun RoomDetailsEditView(
actions = state.avatarActions, actions = state.avatarActions,
isVisible = isAvatarActionsSheetVisible.value, isVisible = isAvatarActionsSheetVisible.value,
onDismiss = { isAvatarActionsSheetVisible.value = false }, onDismiss = { isAvatarActionsSheetVisible.value = false },
onSelectAction = { state.eventSink(RoomDetailsEditEvents.HandleAvatarAction(it)) } onSelectAction = { state.eventSink(RoomDetailsEditEvent.HandleAvatarAction(it)) }
) )
AsyncActionView( AsyncActionView(
async = state.saveAction, async = state.saveAction,
@ -159,14 +159,15 @@ fun RoomDetailsEditView(
confirmationDialog = { confirmationDialog = {
if (state.saveAction == AsyncAction.ConfirmingCancellation) { if (state.saveAction == AsyncAction.ConfirmingCancellation) {
SaveChangesDialog( SaveChangesDialog(
onSubmitClick = { state.eventSink(RoomDetailsEditEvents.OnBackPress) }, onSaveClick = { state.eventSink(RoomDetailsEditEvent.Save) },
onDismiss = { state.eventSink(RoomDetailsEditEvents.CloseDialog) } onDiscardClick = { state.eventSink(RoomDetailsEditEvent.OnBackPress) },
onDismiss = { state.eventSink(RoomDetailsEditEvent.CloseDialog) }
) )
} }
}, },
onSuccess = { onDone() }, onSuccess = { onDone() },
errorMessage = { stringResource(R.string.screen_room_details_edition_error) }, errorMessage = { stringResource(R.string.screen_room_details_edition_error) },
onErrorDismiss = { state.eventSink(RoomDetailsEditEvents.CloseDialog) } onErrorDismiss = { state.eventSink(RoomDetailsEditEvent.CloseDialog) }
) )
PermissionsView( PermissionsView(

View file

@ -241,25 +241,25 @@ class RoomDetailsEditPresenterTest {
assertThat(initialState.roomTopic).isEqualTo("My topic") assertThat(initialState.roomTopic).isEqualTo("My topic")
assertThat(initialState.roomRawName).isEqualTo("Name") assertThat(initialState.roomRawName).isEqualTo("Name")
assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name II")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name II"))
awaitItem().apply { awaitItem().apply {
assertThat(roomTopic).isEqualTo("My topic") assertThat(roomTopic).isEqualTo("My topic")
assertThat(roomRawName).isEqualTo("Name II") assertThat(roomRawName).isEqualTo("Name II")
assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
} }
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name III")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name III"))
awaitItem().apply { awaitItem().apply {
assertThat(roomTopic).isEqualTo("My topic") assertThat(roomTopic).isEqualTo("My topic")
assertThat(roomRawName).isEqualTo("Name III") assertThat(roomRawName).isEqualTo("Name III")
assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
} }
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("Another topic")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("Another topic"))
awaitItem().apply { awaitItem().apply {
assertThat(roomTopic).isEqualTo("Another topic") assertThat(roomTopic).isEqualTo("Another topic")
assertThat(roomRawName).isEqualTo("Name III") assertThat(roomRawName).isEqualTo("Name III")
assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
} }
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply { awaitItem().apply {
assertThat(roomTopic).isEqualTo("Another topic") assertThat(roomTopic).isEqualTo("Another topic")
assertThat(roomRawName).isEqualTo("Name III") assertThat(roomRawName).isEqualTo("Name III")
@ -285,7 +285,7 @@ class RoomDetailsEditPresenterTest {
presenter.test { presenter.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply { awaitItem().apply {
assertThat(roomAvatarUrl).isEqualTo(anotherAvatarUri.toString()) assertThat(roomAvatarUrl).isEqualTo(anotherAvatarUri.toString())
} }
@ -312,7 +312,7 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(initialState.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
assertThat(initialState.cameraPermissionState.permissionGranted).isFalse() assertThat(initialState.cameraPermissionState.permissionGranted).isFalse()
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto)) initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.TakePhoto))
val stateWithAskingPermission = awaitItem() val stateWithAskingPermission = awaitItem()
assertThat(stateWithAskingPermission.cameraPermissionState.showDialog).isTrue() assertThat(stateWithAskingPermission.cameraPermissionState.showDialog).isTrue()
fakePermissionsPresenter.setPermissionGranted() fakePermissionsPresenter.setPermissionGranted()
@ -322,7 +322,7 @@ class RoomDetailsEditPresenterTest {
assertThat(stateWithNewAvatar.roomAvatarUrl).isEqualTo(anotherAvatarUri.toString()) assertThat(stateWithNewAvatar.roomAvatarUrl).isEqualTo(anotherAvatarUri.toString())
// Do it again, no permission is requested // Do it again, no permission is requested
fakePickerProvider.givenResult(roomAvatarUri) fakePickerProvider.givenResult(roomAvatarUri)
stateWithNewAvatar.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto)) stateWithNewAvatar.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.TakePhoto))
val stateWithNewAvatar2 = awaitItem() val stateWithNewAvatar2 = awaitItem()
assertThat(stateWithNewAvatar2.roomAvatarUrl).isEqualTo(AN_AVATAR_URL) assertThat(stateWithNewAvatar2.roomAvatarUrl).isEqualTo(AN_AVATAR_URL)
deleteCallback.assertions().isCalledExactly(3).withSequence( deleteCallback.assertions().isCalledExactly(3).withSequence(
@ -351,32 +351,32 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.saveButtonEnabled).isFalse() assertThat(initialState.saveButtonEnabled).isFalse()
// Once a change is made, the save button is enabled // Once a change is made, the save button is enabled
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name II")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name II"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
} }
// If it's reverted then the save disables again // If it's reverted then the save disables again
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isFalse() assertThat(saveButtonEnabled).isFalse()
} }
// Make a change... // Make a change...
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("Another topic")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("Another topic"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
} }
// Revert it... // Revert it...
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("My topic")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("My topic"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isFalse() assertThat(saveButtonEnabled).isFalse()
} }
// Make a change... // Make a change...
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
} }
// Revert it... // Revert it...
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isFalse() assertThat(saveButtonEnabled).isFalse()
} }
@ -401,32 +401,32 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.saveButtonEnabled).isFalse() assertThat(initialState.saveButtonEnabled).isFalse()
// Once a change is made, the save button is enabled // Once a change is made, the save button is enabled
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name II")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name II"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
} }
// If it's reverted then the save disables again // If it's reverted then the save disables again
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("fallback")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("fallback"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isFalse() assertThat(saveButtonEnabled).isFalse()
} }
// Make a change... // Make a change...
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("Another topic")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("Another topic"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
} }
// Revert it... // Revert it...
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic(""))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isFalse() assertThat(saveButtonEnabled).isFalse()
} }
// Make a change... // Make a change...
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
} }
// Revert it... // Revert it...
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isFalse() assertThat(saveButtonEnabled).isFalse()
} }
@ -454,10 +454,10 @@ class RoomDetailsEditPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("New name")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("New name"))
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("New topic")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("New topic"))
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove))
initialState.eventSink(RoomDetailsEditEvents.Save) initialState.eventSink(RoomDetailsEditEvent.Save)
skipItems(5) skipItems(5)
setNameResult.assertions().isCalledOnce().with(value("New name")) setNameResult.assertions().isCalledOnce().with(value("New name"))
setTopicResult.assertions().isCalledOnce().with(value("New topic")) setTopicResult.assertions().isCalledOnce().with(value("New topic"))
@ -480,9 +480,9 @@ class RoomDetailsEditPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName(" Name ")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName(" Name "))
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(" My topic ")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic(" My topic "))
initialState.eventSink(RoomDetailsEditEvents.Save) initialState.eventSink(RoomDetailsEditEvent.Save)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@ -502,8 +502,8 @@ class RoomDetailsEditPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic(""))
initialState.eventSink(RoomDetailsEditEvents.Save) initialState.eventSink(RoomDetailsEditEvent.Save)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
deleteCallback.assertions().isCalledOnce().with(value(null)) deleteCallback.assertions().isCalledOnce().with(value(null))
} }
@ -524,8 +524,8 @@ class RoomDetailsEditPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName(""))
initialState.eventSink(RoomDetailsEditEvents.Save) initialState.eventSink(RoomDetailsEditEvent.Save)
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
deleteCallback.assertions().isCalledOnce().with(value(null)) deleteCallback.assertions().isCalledOnce().with(value(null))
} }
@ -549,8 +549,8 @@ class RoomDetailsEditPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
initialState.eventSink(RoomDetailsEditEvents.Save) initialState.eventSink(RoomDetailsEditEvent.Save)
skipItems(4) skipItems(4)
updateAvatarResult.assertions().isCalledOnce().with(value(MimeTypes.Jpeg), value(fakeFileContents)) updateAvatarResult.assertions().isCalledOnce().with(value(MimeTypes.Jpeg), value(fakeFileContents))
deleteCallback.assertions().isCalledExactly(2).withSequence( deleteCallback.assertions().isCalledExactly(2).withSequence(
@ -577,8 +577,8 @@ class RoomDetailsEditPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
initialState.eventSink(RoomDetailsEditEvents.Save) initialState.eventSink(RoomDetailsEditEvent.Save)
skipItems(3) skipItems(3)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
} }
@ -593,7 +593,7 @@ class RoomDetailsEditPresenterTest {
setNameResult = { Result.failure(RuntimeException("!")) }, setNameResult = { Result.failure(RuntimeException("!")) },
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomName("New name"), deleteCallbackNumberOfInvocation = 1) saveAndAssertFailure(room, RoomDetailsEditEvent.UpdateRoomName("New name"), deleteCallbackNumberOfInvocation = 1)
} }
@Test @Test
@ -605,7 +605,7 @@ class RoomDetailsEditPresenterTest {
setTopicResult = { Result.failure(RuntimeException("!")) }, setTopicResult = { Result.failure(RuntimeException("!")) },
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomTopic("New topic"), deleteCallbackNumberOfInvocation = 1) saveAndAssertFailure(room, RoomDetailsEditEvent.UpdateRoomTopic("New topic"), deleteCallbackNumberOfInvocation = 1)
} }
@Test @Test
@ -617,7 +617,7 @@ class RoomDetailsEditPresenterTest {
removeAvatarResult = { Result.failure(RuntimeException("!")) }, removeAvatarResult = { Result.failure(RuntimeException("!")) },
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 2) saveAndAssertFailure(room, RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove), deleteCallbackNumberOfInvocation = 2)
} }
@Test @Test
@ -630,7 +630,7 @@ class RoomDetailsEditPresenterTest {
updateAvatarResult = { _, _ -> Result.failure(RuntimeException("!")) }, updateAvatarResult = { _, _ -> Result.failure(RuntimeException("!")) },
canSendStateResult = { _, _ -> Result.success(true) } canSendStateResult = { _, _ -> Result.success(true) }
) )
saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 2) saveAndAssertFailure(room, RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.ChoosePhoto), deleteCallbackNumberOfInvocation = 2)
} }
@Test @Test
@ -650,11 +650,11 @@ class RoomDetailsEditPresenterTest {
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("foo")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomTopic("foo"))
initialState.eventSink(RoomDetailsEditEvents.Save) initialState.eventSink(RoomDetailsEditEvent.Save)
skipItems(3) skipItems(3)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)
initialState.eventSink(RoomDetailsEditEvents.CloseDialog) initialState.eventSink(RoomDetailsEditEvent.CloseDialog)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Uninitialized::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
} }
} }
@ -674,14 +674,14 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.saveButtonEnabled).isFalse() assertThat(initialState.saveButtonEnabled).isFalse()
// Once a change is made, the save button is enabled // Once a change is made, the save button is enabled
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name edited")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name edited"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
eventSink(RoomDetailsEditEvents.OnBackPress) eventSink(RoomDetailsEditEvent.OnBackPress)
} }
awaitItem().apply { awaitItem().apply {
assertThat(saveAction).isEqualTo(AsyncAction.ConfirmingCancellation) assertThat(saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
eventSink(RoomDetailsEditEvents.CloseDialog) eventSink(RoomDetailsEditEvent.CloseDialog)
} }
awaitItem().apply { awaitItem().apply {
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
@ -702,7 +702,7 @@ class RoomDetailsEditPresenterTest {
presenter.test { presenter.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.saveButtonEnabled).isFalse() assertThat(initialState.saveButtonEnabled).isFalse()
initialState.eventSink(RoomDetailsEditEvents.OnBackPress) initialState.eventSink(RoomDetailsEditEvent.OnBackPress)
assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Success(Unit)) assertThat(awaitItem().saveAction).isEqualTo(AsyncAction.Success(Unit))
} }
} }
@ -721,14 +721,14 @@ class RoomDetailsEditPresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.saveButtonEnabled).isFalse() assertThat(initialState.saveButtonEnabled).isFalse()
// Once a change is made, the save button is enabled // Once a change is made, the save button is enabled
initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name edited")) initialState.eventSink(RoomDetailsEditEvent.UpdateRoomName("Name edited"))
awaitItem().apply { awaitItem().apply {
assertThat(saveButtonEnabled).isTrue() assertThat(saveButtonEnabled).isTrue()
eventSink(RoomDetailsEditEvents.OnBackPress) eventSink(RoomDetailsEditEvent.OnBackPress)
} }
awaitItem().apply { awaitItem().apply {
assertThat(saveAction).isEqualTo(AsyncAction.ConfirmingCancellation) assertThat(saveAction).isEqualTo(AsyncAction.ConfirmingCancellation)
eventSink(RoomDetailsEditEvents.OnBackPress) eventSink(RoomDetailsEditEvent.OnBackPress)
} }
awaitItem().apply { awaitItem().apply {
assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit)) assertThat(saveAction).isEqualTo(AsyncAction.Success(Unit))
@ -738,7 +738,7 @@ class RoomDetailsEditPresenterTest {
private suspend fun saveAndAssertFailure( private suspend fun saveAndAssertFailure(
room: JoinedRoom, room: JoinedRoom,
event: RoomDetailsEditEvents, event: RoomDetailsEditEvent,
deleteCallbackNumberOfInvocation: Int = 2, deleteCallbackNumberOfInvocation: Int = 2,
) { ) {
val deleteCallback = lambdaRecorder<Uri?, Unit> {} val deleteCallback = lambdaRecorder<Uri?, Unit> {}
@ -749,7 +749,7 @@ class RoomDetailsEditPresenterTest {
presenter.test { presenter.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(event) initialState.eventSink(event)
initialState.eventSink(RoomDetailsEditEvents.Save) initialState.eventSink(RoomDetailsEditEvent.Save)
skipItems(1) skipItems(1)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Failure::class.java)

View file

@ -39,45 +39,45 @@ class RoomDetailsEditViewTest {
@Test @Test
fun `clicking on back emits the expected Event`() { fun `clicking on back emits the expected Event`() {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder eventSink = eventsRecorder
), ),
) )
rule.pressBack() rule.pressBack()
eventsRecorder.assertSingle(RoomDetailsEditEvents.OnBackPress) eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress)
} }
@Test @Test
fun `clicking on OK when confirming exit emits the expected Event`() { fun `clicking on discard when confirming exit emits the expected Event`() {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_ok) rule.clickOn(CommonStrings.action_discard)
eventsRecorder.assertSingle(RoomDetailsEditEvents.OnBackPress) eventsRecorder.assertSingle(RoomDetailsEditEvent.OnBackPress)
} }
@Test @Test
fun `clicking on cancel when confirming exit emits the expected Event`() { fun `clicking on save when confirming exit emits the expected Event`() {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
saveAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
rule.clickOn(CommonStrings.action_cancel) rule.clickOn(CommonStrings.action_save, inDialog = true)
eventsRecorder.assertSingle(RoomDetailsEditEvents.CloseDialog) eventsRecorder.assertSingle(RoomDetailsEditEvent.Save)
} }
@Test @Test
fun `when edition is successful, the expected callback is invoked`() { fun `when edition is successful, the expected callback is invoked`() {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
@ -91,7 +91,7 @@ class RoomDetailsEditViewTest {
@Test @Test
fun `when name is changed, the expected Event is emitted`() { fun `when name is changed, the expected Event is emitted`() {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -99,12 +99,12 @@ class RoomDetailsEditViewTest {
), ),
) )
rule.onNodeWithText("Marketing").performTextInput("A") rule.onNodeWithText("Marketing").performTextInput("A")
eventsRecorder.assertSingle(RoomDetailsEditEvents.UpdateRoomName("AMarketing")) eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomName("AMarketing"))
} }
@Test @Test
fun `when user cannot change name, nothing happen`() { fun `when user cannot change name, nothing happen`() {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -117,7 +117,7 @@ class RoomDetailsEditViewTest {
@Test @Test
fun `when topic is changed, the expected Event is emitted`() { fun `when topic is changed, the expected Event is emitted`() {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -125,12 +125,12 @@ class RoomDetailsEditViewTest {
), ),
) )
rule.onNodeWithText("My Topic").performTextInput("A") rule.onNodeWithText("My Topic").performTextInput("A")
eventsRecorder.assertSingle(RoomDetailsEditEvents.UpdateRoomTopic("AMy Topic")) eventsRecorder.assertSingle(RoomDetailsEditEvent.UpdateRoomTopic("AMy Topic"))
} }
@Test @Test
fun `when user cannot change topic, nothing happen`() { fun `when user cannot change topic, nothing happen`() {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -146,7 +146,7 @@ class RoomDetailsEditViewTest {
fun `when avatar is changed with action to take photo, the expected Event is emitted`() { fun `when avatar is changed with action to take photo, the expected Event is emitted`() {
testAvatarChange( testAvatarChange(
stringActionRes = CommonStrings.action_take_photo, 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`() { fun `when avatar is changed with action to choose photo, the expected Event is emitted`() {
testAvatarChange( testAvatarChange(
stringActionRes = CommonStrings.action_choose_photo, 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`() { fun `when avatar is changed with action to remove photo, the expected Event is emitted`() {
testAvatarChange( testAvatarChange(
stringActionRes = CommonStrings.action_remove, stringActionRes = CommonStrings.action_remove,
expectedEvent = RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove), expectedEvent = RoomDetailsEditEvent.HandleAvatarAction(AvatarAction.Remove),
) )
} }
private fun testAvatarChange( private fun testAvatarChange(
@StringRes stringActionRes: Int, @StringRes stringActionRes: Int,
expectedEvent: RoomDetailsEditEvents.HandleAvatarAction, expectedEvent: RoomDetailsEditEvent.HandleAvatarAction,
) { ) {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -187,7 +187,7 @@ class RoomDetailsEditViewTest {
@Test @Test
fun `when user cannot change avatar, nothing happen`() { fun `when user cannot change avatar, nothing happen`() {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>(expectEvents = false)
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -200,7 +200,7 @@ class RoomDetailsEditViewTest {
@Test @Test
fun `when save is clicked, the expected Event is emitted`() { fun `when save is clicked, the expected Event is emitted`() {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -208,12 +208,12 @@ class RoomDetailsEditViewTest {
), ),
) )
rule.clickOn(CommonStrings.action_save) rule.clickOn(CommonStrings.action_save)
eventsRecorder.assertSingle(RoomDetailsEditEvents.Save) eventsRecorder.assertSingle(RoomDetailsEditEvent.Save)
} }
@Test @Test
fun `when save is clicked, but nothing need to be saved, nothing happens`() { 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( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -225,7 +225,7 @@ class RoomDetailsEditViewTest {
@Test @Test
fun `when error is shown, closing the dialog emit the expected Event`() { fun `when error is shown, closing the dialog emit the expected Event`() {
val eventsRecorder = EventsRecorder<RoomDetailsEditEvents>() val eventsRecorder = EventsRecorder<RoomDetailsEditEvent>()
rule.setRoomDetailsEditView( rule.setRoomDetailsEditView(
aRoomDetailsEditState( aRoomDetailsEditState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
@ -233,7 +233,7 @@ class RoomDetailsEditViewTest {
), ),
) )
rule.clickOn(CommonStrings.action_ok) rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(RoomDetailsEditEvents.CloseDialog) eventsRecorder.assertSingle(RoomDetailsEditEvent.CloseDialog)
} }
} }

View file

@ -8,6 +8,19 @@
package io.element.android.features.securityandprivacy.api 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
}

View file

@ -17,7 +17,11 @@ import io.element.android.libraries.di.RoomScope
@ContributesBinding(RoomScope::class) @ContributesBinding(RoomScope::class)
class DefaultSecurityAndPrivacyEntryPoint : SecurityAndPrivacyEntryPoint { class DefaultSecurityAndPrivacyEntryPoint : SecurityAndPrivacyEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node { override fun createNode(
return parentNode.createNode<SecurityAndPrivacyFlowNode>(buildContext) parentNode: Node,
buildContext: BuildContext,
callback: SecurityAndPrivacyEntryPoint.Callback,
): Node {
return parentNode.createNode<SecurityAndPrivacyFlowNode>(buildContext, listOf(callback))
} }
} }

View file

@ -18,10 +18,12 @@ import com.bumble.appyx.navmodel.backstack.BackStack
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode 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.editroomaddress.EditRoomAddressNode
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyNode import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyNode
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode 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.architecture.createNode
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -47,7 +49,8 @@ class SecurityAndPrivacyFlowNode(
data object EditRoomAddress : NavTarget 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 { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {

View file

@ -12,15 +12,22 @@ import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
interface SecurityAndPrivacyNavigator : Plugin { interface SecurityAndPrivacyNavigator : Plugin {
fun onDone()
fun openEditRoomAddress() fun openEditRoomAddress()
fun closeEditRoomAddress() fun closeEditRoomAddress()
} }
class BackstackSecurityAndPrivacyNavigator( class BackstackSecurityAndPrivacyNavigator(
private val callback: SecurityAndPrivacyEntryPoint.Callback,
private val backStack: BackStack<SecurityAndPrivacyFlowNode.NavTarget> private val backStack: BackStack<SecurityAndPrivacyFlowNode.NavTarget>
) : SecurityAndPrivacyNavigator { ) : SecurityAndPrivacyNavigator {
override fun onDone() {
callback.onDone()
}
override fun openEditRoomAddress() { override fun openEditRoomAddress() {
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress) backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress)
} }

View file

@ -8,16 +8,16 @@
package io.element.android.features.securityandprivacy.impl.root package io.element.android.features.securityandprivacy.impl.root
sealed interface SecurityAndPrivacyEvents { sealed interface SecurityAndPrivacyEvent {
data object EditRoomAddress : SecurityAndPrivacyEvents data object EditRoomAddress : SecurityAndPrivacyEvent
data object Save : SecurityAndPrivacyEvents data object Save : SecurityAndPrivacyEvent
data object Exit : SecurityAndPrivacyEvents data object Exit : SecurityAndPrivacyEvent
data object DismissExitConfirmation : SecurityAndPrivacyEvents data object DismissExitConfirmation : SecurityAndPrivacyEvent
data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvents data class ChangeRoomAccess(val roomAccess: SecurityAndPrivacyRoomAccess) : SecurityAndPrivacyEvent
data object ToggleEncryptionState : SecurityAndPrivacyEvents data object ToggleEncryptionState : SecurityAndPrivacyEvent
data object CancelEnableEncryption : SecurityAndPrivacyEvents data object CancelEnableEncryption : SecurityAndPrivacyEvent
data object ConfirmEnableEncryption : SecurityAndPrivacyEvents data object ConfirmEnableEncryption : SecurityAndPrivacyEvent
data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvents data class ChangeHistoryVisibility(val historyVisibility: SecurityAndPrivacyHistoryVisibility) : SecurityAndPrivacyEvent
data object ToggleRoomVisibility : SecurityAndPrivacyEvents data object ToggleRoomVisibility : SecurityAndPrivacyEvent
data object DismissSaveError : SecurityAndPrivacyEvents data object DismissSaveError : SecurityAndPrivacyEvent
} }

View file

@ -40,7 +40,6 @@ class SecurityAndPrivacyNode(
val state by stateFlow.collectAsState() val state by stateFlow.collectAsState()
SecurityAndPrivacyView( SecurityAndPrivacyView(
state = state, state = state,
onBackClick = this::navigateUp,
modifier = modifier modifier = modifier
) )
} }

View file

@ -64,7 +64,6 @@ class SecurityAndPrivacyPresenter(
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock) featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
}.collectAsState(false) }.collectAsState(false)
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) } val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
var confirmExitAction by remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val homeserverName = remember { matrixClient.userIdServerName() } val homeserverName = remember { matrixClient.userIdServerName() }
val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val roomInfo by room.roomInfoFlow.collectAsState() val roomInfo by room.roomInfoFlow.collectAsState()
@ -109,9 +108,9 @@ class SecurityAndPrivacyPresenter(
var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) } var showEnableEncryptionConfirmation by remember(savedSettings.isEncrypted) { mutableStateOf(false) }
val permissions by room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value) val permissions by room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value)
fun handleEvent(event: SecurityAndPrivacyEvents) { fun handleEvent(event: SecurityAndPrivacyEvent) {
when (event) { when (event) {
SecurityAndPrivacyEvents.Save -> { SecurityAndPrivacyEvent.Save -> {
coroutineScope.save( coroutineScope.save(
saveAction = saveAction, saveAction = saveAction,
isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory, isVisibleInRoomDirectory = savedIsVisibleInRoomDirectory,
@ -119,49 +118,55 @@ class SecurityAndPrivacyPresenter(
editedSettings = editedSettings editedSettings = editedSettings
) )
} }
is SecurityAndPrivacyEvents.ChangeRoomAccess -> { is SecurityAndPrivacyEvent.ChangeRoomAccess -> {
editedRoomAccess = event.roomAccess editedRoomAccess = event.roomAccess
} }
is SecurityAndPrivacyEvents.ToggleEncryptionState -> { is SecurityAndPrivacyEvent.ToggleEncryptionState -> {
if (editedIsEncrypted) { if (editedIsEncrypted) {
editedIsEncrypted = false editedIsEncrypted = false
} else { } else {
showEnableEncryptionConfirmation = true showEnableEncryptionConfirmation = true
} }
} }
is SecurityAndPrivacyEvents.ChangeHistoryVisibility -> { is SecurityAndPrivacyEvent.ChangeHistoryVisibility -> {
editedHistoryVisibility = event.historyVisibility editedHistoryVisibility = event.historyVisibility
} }
SecurityAndPrivacyEvents.ToggleRoomVisibility -> { SecurityAndPrivacyEvent.ToggleRoomVisibility -> {
editedVisibleInRoomDirectory = when (val edited = editedVisibleInRoomDirectory) { editedVisibleInRoomDirectory = when (val edited = editedVisibleInRoomDirectory) {
is AsyncData.Success -> AsyncData.Success(!edited.data) is AsyncData.Success -> AsyncData.Success(!edited.data)
else -> edited else -> edited
} }
} }
SecurityAndPrivacyEvents.EditRoomAddress -> navigator.openEditRoomAddress() SecurityAndPrivacyEvent.EditRoomAddress -> navigator.openEditRoomAddress()
SecurityAndPrivacyEvents.CancelEnableEncryption -> { SecurityAndPrivacyEvent.CancelEnableEncryption -> {
showEnableEncryptionConfirmation = false showEnableEncryptionConfirmation = false
} }
SecurityAndPrivacyEvents.ConfirmEnableEncryption -> { SecurityAndPrivacyEvent.ConfirmEnableEncryption -> {
showEnableEncryptionConfirmation = false showEnableEncryptionConfirmation = false
editedIsEncrypted = true editedIsEncrypted = true
} }
SecurityAndPrivacyEvents.DismissSaveError -> { SecurityAndPrivacyEvent.DismissSaveError -> {
saveAction.value = AsyncAction.Uninitialized saveAction.value = AsyncAction.Uninitialized
} }
SecurityAndPrivacyEvents.Exit -> { SecurityAndPrivacyEvent.Exit -> {
confirmExitAction = if (savedSettings == editedSettings || confirmExitAction.isConfirming()) { saveAction.value = if (savedSettings == editedSettings || saveAction.value == AsyncAction.ConfirmingCancellation) {
AsyncAction.Success(Unit) AsyncAction.Success(Unit)
} else { } else {
AsyncAction.ConfirmingNoParams AsyncAction.ConfirmingCancellation
} }
} }
SecurityAndPrivacyEvents.DismissExitConfirmation -> { SecurityAndPrivacyEvent.DismissExitConfirmation -> {
confirmExitAction = AsyncAction.Uninitialized saveAction.value = AsyncAction.Uninitialized
} }
} }
} }
LaunchedEffect(saveAction.value.isSuccess()) {
if (saveAction.value.isSuccess()) {
navigator.onDone()
}
}
val state = SecurityAndPrivacyState( val state = SecurityAndPrivacyState(
savedSettings = savedSettings, savedSettings = savedSettings,
editedSettings = editedSettings, editedSettings = editedSettings,
@ -171,7 +176,6 @@ class SecurityAndPrivacyPresenter(
saveAction = saveAction.value, saveAction = saveAction.value,
permissions = permissions, permissions = permissions,
isSpace = roomInfo.isSpace, isSpace = roomInfo.isSpace,
confirmExitAction = confirmExitAction,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )

View file

@ -22,10 +22,9 @@ data class SecurityAndPrivacyState(
val showEnableEncryptionConfirmation: Boolean, val showEnableEncryptionConfirmation: Boolean,
val isKnockEnabled: Boolean, val isKnockEnabled: Boolean,
val saveAction: AsyncAction<Unit>, val saveAction: AsyncAction<Unit>,
val confirmExitAction: AsyncAction<Unit>,
val isSpace: Boolean, val isSpace: Boolean,
private val permissions: SecurityAndPrivacyPermissions, private val permissions: SecurityAndPrivacyPermissions,
val eventSink: (SecurityAndPrivacyEvents) -> Unit val eventSink: (SecurityAndPrivacyEvent) -> Unit
) { ) {
val canBeSaved = savedSettings != editedSettings val canBeSaved = savedSettings != editedSettings

View file

@ -27,7 +27,7 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAn
isSpace = false, isSpace = false,
), ),
aSecurityAndPrivacyState( aSecurityAndPrivacyState(
confirmExitAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
isSpace = false, isSpace = false,
), ),
aSecurityAndPrivacyState( aSecurityAndPrivacyState(
@ -109,7 +109,6 @@ fun aSecurityAndPrivacyState(
homeserverName: String = "myserver.xyz", homeserverName: String = "myserver.xyz",
showEncryptionConfirmation: Boolean = false, showEncryptionConfirmation: Boolean = false,
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized, saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
confirmExitAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
permissions: SecurityAndPrivacyPermissions = SecurityAndPrivacyPermissions( permissions: SecurityAndPrivacyPermissions = SecurityAndPrivacyPermissions(
canChangeRoomAccess = true, canChangeRoomAccess = true,
canChangeHistoryVisibility = true, canChangeHistoryVisibility = true,
@ -118,14 +117,13 @@ fun aSecurityAndPrivacyState(
), ),
isKnockEnabled: Boolean = true, isKnockEnabled: Boolean = true,
isSpace: Boolean = false, isSpace: Boolean = false,
eventSink: (SecurityAndPrivacyEvents) -> Unit = {} eventSink: (SecurityAndPrivacyEvent) -> Unit = {}
) = SecurityAndPrivacyState( ) = SecurityAndPrivacyState(
editedSettings = editedSettings, editedSettings = editedSettings,
savedSettings = savedSettings, savedSettings = savedSettings,
homeserverName = homeserverName, homeserverName = homeserverName,
showEnableEncryptionConfirmation = showEncryptionConfirmation, showEnableEncryptionConfirmation = showEncryptionConfirmation,
saveAction = saveAction, saveAction = saveAction,
confirmExitAction = confirmExitAction,
isKnockEnabled = isKnockEnabled, isKnockEnabled = isKnockEnabled,
permissions = permissions, permissions = permissions,
isSpace = isSpace, isSpace = isSpace,

View file

@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.securityandprivacy.impl.R 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.AsyncData
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView
@ -56,11 +57,10 @@ import kotlinx.collections.immutable.ImmutableSet
@Composable @Composable
fun SecurityAndPrivacyView( fun SecurityAndPrivacyView(
state: SecurityAndPrivacyState, state: SecurityAndPrivacyState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
BackHandler { BackHandler {
state.eventSink(SecurityAndPrivacyEvents.Exit) state.eventSink(SecurityAndPrivacyEvent.Exit)
} }
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
@ -68,10 +68,10 @@ fun SecurityAndPrivacyView(
SecurityAndPrivacyToolbar( SecurityAndPrivacyToolbar(
isSaveActionEnabled = state.canBeSaved, isSaveActionEnabled = state.canBeSaved,
onBackClick = { onBackClick = {
state.eventSink(SecurityAndPrivacyEvents.Exit) state.eventSink(SecurityAndPrivacyEvent.Exit)
}, },
onSaveClick = { onSaveClick = {
state.eventSink(SecurityAndPrivacyEvents.Save) state.eventSink(SecurityAndPrivacyEvent.Save)
}, },
) )
} }
@ -90,7 +90,7 @@ fun SecurityAndPrivacyView(
edited = state.editedSettings.roomAccess, edited = state.editedSettings.roomAccess,
saved = state.savedSettings.roomAccess, saved = state.savedSettings.roomAccess,
isKnockEnabled = state.isKnockEnabled, isKnockEnabled = state.isKnockEnabled,
onSelectOption = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) }, onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(it)) },
) )
} }
if (state.showRoomVisibilitySections) { if (state.showRoomVisibilitySections) {
@ -98,10 +98,10 @@ fun SecurityAndPrivacyView(
RoomAddressSection( RoomAddressSection(
roomAddress = state.editedSettings.address, roomAddress = state.editedSettings.address,
homeserverName = state.homeserverName, homeserverName = state.homeserverName,
onRoomAddressClick = { state.eventSink(SecurityAndPrivacyEvents.EditRoomAddress) }, onRoomAddressClick = { state.eventSink(SecurityAndPrivacyEvent.EditRoomAddress) },
isVisibleInRoomDirectory = state.editedSettings.isVisibleInRoomDirectory, isVisibleInRoomDirectory = state.editedSettings.isVisibleInRoomDirectory,
onVisibilityChange = { onVisibilityChange = {
state.eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility) state.eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
}, },
) )
} }
@ -110,10 +110,10 @@ fun SecurityAndPrivacyView(
isRoomEncrypted = state.editedSettings.isEncrypted, isRoomEncrypted = state.editedSettings.isEncrypted,
// encryption can't be disabled once enabled // encryption can't be disabled once enabled
canToggleEncryption = !state.savedSettings.isEncrypted, canToggleEncryption = !state.savedSettings.isEncrypted,
onToggleEncryption = { state.eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) }, onToggleEncryption = { state.eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState) },
showConfirmation = state.showEnableEncryptionConfirmation, showConfirmation = state.showEnableEncryptionConfirmation,
onDismissConfirmation = { state.eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) }, onDismissConfirmation = { state.eventSink(SecurityAndPrivacyEvent.CancelEnableEncryption) },
onConfirmEncryption = { state.eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) }, onConfirmEncryption = { state.eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption) },
) )
} }
if (state.showHistoryVisibilitySection) { if (state.showHistoryVisibilitySection) {
@ -121,7 +121,7 @@ fun SecurityAndPrivacyView(
editedOption = state.editedSettings.historyVisibility, editedOption = state.editedSettings.historyVisibility,
savedOptions = state.savedSettings.historyVisibility, savedOptions = state.savedSettings.historyVisibility,
availableOptions = state.availableHistoryVisibilities, availableOptions = state.availableHistoryVisibilities,
onSelectOption = { state.eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(it)) }, onSelectOption = { state.eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(it)) },
) )
} }
} }
@ -129,25 +129,24 @@ fun SecurityAndPrivacyView(
AsyncActionView( AsyncActionView(
async = state.saveAction, async = state.saveAction,
onSuccess = { }, 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) }, errorMessage = { stringResource(CommonStrings.error_unknown) },
progressDialog = { progressDialog = {
AsyncActionViewDefaults.ProgressDialog( AsyncActionViewDefaults.ProgressDialog(
progressText = stringResource(CommonStrings.common_saving), progressText = stringResource(CommonStrings.common_saving),
) )
}, },
onRetry = { state.eventSink(SecurityAndPrivacyEvents.Save) }, onRetry = { state.eventSink(SecurityAndPrivacyEvent.Save) },
)
AsyncActionView(
async = state.confirmExitAction,
onSuccess = { onBackClick() },
onErrorDismiss = { },
confirmationDialog = {
SaveChangesDialog(
onSubmitClick = { state.eventSink(SecurityAndPrivacyEvents.Exit) },
onDismiss = { state.eventSink(SecurityAndPrivacyEvents.DismissExitConfirmation) }
)
},
) )
} }
@ -425,6 +424,5 @@ internal fun SecurityAndPrivacyViewDarkPreview(@PreviewParameter(SecurityAndPriv
private fun ContentToPreview(state: SecurityAndPrivacyState) { private fun ContentToPreview(state: SecurityAndPrivacyState) {
SecurityAndPrivacyView( SecurityAndPrivacyView(
state = state, state = state,
onBackClick = {},
) )
} }

View file

@ -11,9 +11,14 @@ package io.element.android.features.securityandprivacy.impl
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
class FakeSecurityAndPrivacyNavigator( class FakeSecurityAndPrivacyNavigator(
private val onDoneLambda: () -> Unit = { lambdaError() },
private val openEditRoomAddressLambda: () -> Unit = { lambdaError() }, private val openEditRoomAddressLambda: () -> Unit = { lambdaError() },
private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() }, private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() },
) : SecurityAndPrivacyNavigator { ) : SecurityAndPrivacyNavigator {
override fun onDone() {
onDoneLambda()
}
override fun openEditRoomAddress() { override fun openEditRoomAddress() {
openEditRoomAddressLambda() openEditRoomAddressLambda()
} }

View file

@ -9,7 +9,7 @@
package io.element.android.features.securityandprivacy.impl package io.element.android.features.securityandprivacy.impl
import com.google.common.truth.Truth.assertThat 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.SecurityAndPrivacyHistoryVisibility
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyPresenter import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyPresenter
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess
@ -96,13 +96,13 @@ class SecurityAndPrivacyPresenterTest {
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
assertThat(showRoomVisibilitySections).isFalse() assertThat(showRoomVisibilitySections).isFalse()
eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone) assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.Anyone)
assertThat(showRoomVisibilitySections).isTrue() assertThat(showRoomVisibilitySections).isTrue()
assertThat(canBeSaved).isTrue() assertThat(canBeSaved).isTrue()
eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly)) eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.InviteOnly))
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
@ -119,12 +119,12 @@ class SecurityAndPrivacyPresenterTest {
skipItems(1) skipItems(1)
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection) assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection)
eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceInvite)) eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceInvite))
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceInvite) assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceInvite)
assertThat(canBeSaved).isTrue() assertThat(canBeSaved).isTrue()
eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection)) eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.SinceSelection))
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection) assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.SinceSelection)
@ -140,26 +140,26 @@ class SecurityAndPrivacyPresenterTest {
skipItems(1) skipItems(1)
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.isEncrypted).isFalse() assertThat(editedSettings.isEncrypted).isFalse()
eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState)
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(showEnableEncryptionConfirmation).isTrue() assertThat(showEnableEncryptionConfirmation).isTrue()
eventSink(SecurityAndPrivacyEvents.CancelEnableEncryption) eventSink(SecurityAndPrivacyEvent.CancelEnableEncryption)
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(showEnableEncryptionConfirmation).isFalse() assertThat(showEnableEncryptionConfirmation).isFalse()
eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState)
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(showEnableEncryptionConfirmation).isTrue() assertThat(showEnableEncryptionConfirmation).isTrue()
eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
} }
skipItems(1) skipItems(1)
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.isEncrypted).isTrue() assertThat(editedSettings.isEncrypted).isTrue()
assertThat(showEnableEncryptionConfirmation).isFalse() assertThat(showEnableEncryptionConfirmation).isFalse()
assertThat(canBeSaved).isTrue() assertThat(canBeSaved).isTrue()
eventSink(SecurityAndPrivacyEvents.ToggleEncryptionState) eventSink(SecurityAndPrivacyEvent.ToggleEncryptionState)
} }
skipItems(1) skipItems(1)
with(awaitItem()) { with(awaitItem()) {
@ -186,12 +186,12 @@ class SecurityAndPrivacyPresenterTest {
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false)) assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false))
eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility) eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true))
assertThat(canBeSaved).isTrue() assertThat(canBeSaved).isTrue()
eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility) eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false)) assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(false))
@ -203,12 +203,12 @@ class SecurityAndPrivacyPresenterTest {
@Test @Test
fun `present - edit room address`() = runTest { fun `present - edit room address`() = runTest {
val openEditRoomAddressLambda = lambdaRecorder<Unit> { } val openEditRoomAddressLambda = lambdaRecorder<Unit> { }
val navigator = FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda) val navigator = FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda = openEditRoomAddressLambda)
val presenter = createSecurityAndPrivacyPresenter(navigator = navigator) val presenter = createSecurityAndPrivacyPresenter(navigator = navigator)
presenter.test { presenter.test {
skipItems(1) skipItems(1)
with(awaitItem()) { with(awaitItem()) {
eventSink(SecurityAndPrivacyEvents.EditRoomAddress) eventSink(SecurityAndPrivacyEvent.EditRoomAddress)
} }
assert(openEditRoomAddressLambda).isCalledOnce() assert(openEditRoomAddressLambda).isCalledOnce()
} }
@ -231,28 +231,35 @@ class SecurityAndPrivacyPresenterTest {
updateRoomVisibilityResult = updateRoomVisibilityLambda, updateRoomVisibilityResult = updateRoomVisibilityLambda,
updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda, 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 { presenter.test {
skipItems(2) skipItems(2)
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
} }
with(awaitItem()) { with(awaitItem()) {
eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone)) eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone))
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone) assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone)
eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
} }
skipItems(1) skipItems(1)
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.isEncrypted).isTrue() assertThat(editedSettings.isEncrypted).isTrue()
eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility) eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true))
eventSink(SecurityAndPrivacyEvents.Save) eventSink(SecurityAndPrivacyEvent.Save)
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Loading) assertThat(saveAction).isEqualTo(AsyncAction.Loading)
@ -276,6 +283,7 @@ class SecurityAndPrivacyPresenterTest {
assert(updateJoinRuleLambda).isCalledOnce() assert(updateJoinRuleLambda).isCalledOnce()
assert(updateRoomVisibilityLambda).isCalledOnce() assert(updateRoomVisibilityLambda).isCalledOnce()
assert(updateRoomHistoryVisibilityLambda).isCalledOnce() assert(updateRoomHistoryVisibilityLambda).isCalledOnce()
onDoneLambda.assertions().isCalledOnce()
} }
} }
@ -303,23 +311,23 @@ class SecurityAndPrivacyPresenterTest {
skipItems(2) skipItems(2)
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly) assertThat(editedSettings.roomAccess).isEqualTo(SecurityAndPrivacyRoomAccess.InviteOnly)
eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone)) eventSink(SecurityAndPrivacyEvent.ChangeRoomAccess(SecurityAndPrivacyRoomAccess.Anyone))
} }
with(awaitItem()) { with(awaitItem()) {
eventSink(SecurityAndPrivacyEvents.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone)) eventSink(SecurityAndPrivacyEvent.ChangeHistoryVisibility(SecurityAndPrivacyHistoryVisibility.Anyone))
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone) assertThat(editedSettings.historyVisibility).isEqualTo(SecurityAndPrivacyHistoryVisibility.Anyone)
eventSink(SecurityAndPrivacyEvents.ConfirmEnableEncryption) eventSink(SecurityAndPrivacyEvent.ConfirmEnableEncryption)
} }
skipItems(1) skipItems(1)
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.isEncrypted).isTrue() assertThat(editedSettings.isEncrypted).isTrue()
eventSink(SecurityAndPrivacyEvents.ToggleRoomVisibility) eventSink(SecurityAndPrivacyEvent.ToggleRoomVisibility)
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true)) assertThat(editedSettings.isVisibleInRoomDirectory).isEqualTo(AsyncData.Success(true))
eventSink(SecurityAndPrivacyEvents.Save) eventSink(SecurityAndPrivacyEvent.Save)
} }
with(awaitItem()) { with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Loading) assertThat(saveAction).isEqualTo(AsyncAction.Loading)
@ -344,7 +352,7 @@ class SecurityAndPrivacyPresenterTest {
assert(updateRoomVisibilityLambda).isCalledOnce() assert(updateRoomVisibilityLambda).isCalledOnce()
assert(updateRoomHistoryVisibilityLambda).isCalledOnce() assert(updateRoomHistoryVisibilityLambda).isCalledOnce()
// Clear error // Clear error
state.eventSink(SecurityAndPrivacyEvents.DismissSaveError) state.eventSink(SecurityAndPrivacyEvent.DismissSaveError)
with(awaitItem()) { with(awaitItem()) {
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
} }

View file

@ -14,7 +14,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4 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.SecurityAndPrivacyHistoryVisibility
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyRoomAccess
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyState 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.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings 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.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBack import io.element.android.tests.testutils.pressBack
@ -40,53 +39,53 @@ class SecurityAndPrivacyViewTest {
@Test @Test
fun `click on back invokes emits the expected event`() { fun `click on back invokes emits the expected event`() {
val recorder = EventsRecorder<SecurityAndPrivacyEvents>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
) )
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.pressBack() rule.pressBack()
recorder.assertSingle(SecurityAndPrivacyEvents.Exit) recorder.assertSingle(SecurityAndPrivacyEvent.Exit)
} }
@Test @Test
fun `confirm cancellation emits the expected event`() { fun `discard cancellation emits the expected event`() {
val recorder = EventsRecorder<SecurityAndPrivacyEvents>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
confirmExitAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = recorder, eventSink = recorder,
) )
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.clickOn(CommonStrings.action_ok) rule.clickOn(CommonStrings.action_discard)
recorder.assertSingle(SecurityAndPrivacyEvents.Exit) recorder.assertSingle(SecurityAndPrivacyEvent.Exit)
} }
@Test @Test
fun `dismiss cancellation confirmation emits the expected event`() { fun `save cancellation confirmation emits the expected event`() {
val recorder = EventsRecorder<SecurityAndPrivacyEvents>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
confirmExitAction = AsyncAction.ConfirmingCancellation, saveAction = AsyncAction.ConfirmingCancellation,
eventSink = recorder, eventSink = recorder,
) )
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.clickOn(CommonStrings.action_cancel) rule.clickOn(CommonStrings.action_save, inDialog = true)
recorder.assertSingle(SecurityAndPrivacyEvents.DismissExitConfirmation) recorder.assertSingle(SecurityAndPrivacyEvent.Save)
} }
@Test @Test
fun `click on room access item emits the expected event`() { fun `click on room access item emits the expected event`() {
val recorder = EventsRecorder<SecurityAndPrivacyEvents>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
) )
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_room_access_invite_only_option_title) 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 @Test
fun `click on disabled save doesn't emit event`() { 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) val state = aSecurityAndPrivacyState(eventSink = recorder)
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.clickOn(CommonStrings.action_save) rule.clickOn(CommonStrings.action_save)
@ -95,7 +94,7 @@ class SecurityAndPrivacyViewTest {
@Test @Test
fun `click on enabled save emits the expected event`() { fun `click on enabled save emits the expected event`() {
val recorder = EventsRecorder<SecurityAndPrivacyEvents>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
editedSettings = aSecurityAndPrivacySettings( editedSettings = aSecurityAndPrivacySettings(
@ -104,14 +103,14 @@ class SecurityAndPrivacyViewTest {
) )
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.clickOn(CommonStrings.action_save) rule.clickOn(CommonStrings.action_save)
recorder.assertSingle(SecurityAndPrivacyEvents.Save) recorder.assertSingle(SecurityAndPrivacyEvent.Save)
} }
@Test @Test
@Config(qualifiers = "h640dp") @Config(qualifiers = "h640dp")
fun `click on room address item emits the expected event`() { fun `click on room address item emits the expected event`() {
val address = "@alias:matrix.org" val address = "@alias:matrix.org"
val recorder = EventsRecorder<SecurityAndPrivacyEvents>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
editedSettings = aSecurityAndPrivacySettings( editedSettings = aSecurityAndPrivacySettings(
@ -121,13 +120,13 @@ class SecurityAndPrivacyViewTest {
) )
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.onNodeWithText(address).performClick() rule.onNodeWithText(address).performClick()
recorder.assertSingle(SecurityAndPrivacyEvents.EditRoomAddress) recorder.assertSingle(SecurityAndPrivacyEvent.EditRoomAddress)
} }
@Test @Test
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
fun `click on room visibility item emits the expected event`() { fun `click on room visibility item emits the expected event`() {
val recorder = EventsRecorder<SecurityAndPrivacyEvents>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
editedSettings = aSecurityAndPrivacySettings( editedSettings = aSecurityAndPrivacySettings(
@ -137,13 +136,13 @@ class SecurityAndPrivacyViewTest {
) )
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title) rule.clickOn(R.string.screen_security_and_privacy_room_directory_visibility_toggle_title)
recorder.assertSingle(SecurityAndPrivacyEvents.ToggleRoomVisibility) recorder.assertSingle(SecurityAndPrivacyEvent.ToggleRoomVisibility)
} }
@Test @Test
@Config(qualifiers = "h640dp") @Config(qualifiers = "h640dp")
fun `click on history visibility item emits the expected event`() { fun `click on history visibility item emits the expected event`() {
val recorder = EventsRecorder<SecurityAndPrivacyEvents>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
editedSettings = aSecurityAndPrivacySettings( editedSettings = aSecurityAndPrivacySettings(
@ -152,32 +151,32 @@ class SecurityAndPrivacyViewTest {
) )
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_room_history_since_selecting_option_title) 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 @Test
@Config(qualifiers = "h640dp") @Config(qualifiers = "h640dp")
fun `click on encryption item emits the expected event`() { fun `click on encryption item emits the expected event`() {
val recorder = EventsRecorder<SecurityAndPrivacyEvents>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
savedSettings = aSecurityAndPrivacySettings(isEncrypted = false), savedSettings = aSecurityAndPrivacySettings(isEncrypted = false),
) )
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_encryption_toggle_title) rule.clickOn(R.string.screen_security_and_privacy_encryption_toggle_title)
recorder.assertSingle(SecurityAndPrivacyEvents.ToggleEncryptionState) recorder.assertSingle(SecurityAndPrivacyEvent.ToggleEncryptionState)
} }
@Test @Test
fun `click on encryption confirm emits the expected event`() { fun `click on encryption confirm emits the expected event`() {
val recorder = EventsRecorder<SecurityAndPrivacyEvents>() val recorder = EventsRecorder<SecurityAndPrivacyEvent>()
val state = aSecurityAndPrivacyState( val state = aSecurityAndPrivacyState(
eventSink = recorder, eventSink = recorder,
showEncryptionConfirmation = true, showEncryptionConfirmation = true,
) )
rule.setSecurityAndPrivacyView(state) rule.setSecurityAndPrivacyView(state)
rule.clickOn(R.string.screen_security_and_privacy_enable_encryption_alert_confirm_button_title) 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( state: SecurityAndPrivacyState = aSecurityAndPrivacyState(
eventSink = EventsRecorder(expectEvents = false), eventSink = EventsRecorder(expectEvents = false),
), ),
onBackClick: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
SecurityAndPrivacyView( SecurityAndPrivacyView(
state = state, state = state,
onBackClick = onBackClick,
) )
} }
} }

View file

@ -13,7 +13,11 @@ import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntr
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
class FakeSecurityAndPrivacyEntryPoint : SecurityAndPrivacyEntryPoint { class FakeSecurityAndPrivacyEntryPoint : SecurityAndPrivacyEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node { override fun createNode(
parentNode: Node,
buildContext: BuildContext,
callback: SecurityAndPrivacyEntryPoint.Callback,
): Node {
lambdaError() lambdaError()
} }
} }

View file

@ -14,6 +14,7 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
@ -95,9 +96,15 @@ class SpaceSettingsFlowNode(
) )
} }
is NavTarget.SecurityAndPrivacy -> { is NavTarget.SecurityAndPrivacy -> {
val callback = object : SecurityAndPrivacyEntryPoint.Callback {
override fun onDone() {
backstack.pop()
}
}
securityAndPrivacyEntryPoint.createNode( securityAndPrivacyEntryPoint.createNode(
parentNode = this, parentNode = this,
buildContext = buildContext, buildContext = buildContext,
callback = callback,
) )
} }
is NavTarget.RolesAndPermissions -> { is NavTarget.RolesAndPermissions -> {

View file

@ -17,16 +17,22 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun SaveChangesDialog( fun SaveChangesDialog(
onSubmitClick: () -> Unit, onSaveClick: () -> Unit,
onDiscardClick: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
title: String = stringResource(CommonStrings.dialog_unsaved_changes_title), 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( ) = ConfirmationDialog(
modifier = modifier, modifier = modifier,
title = title, title = title,
content = content, content = content,
onSubmitClick = onSubmitClick, submitText = submitText,
cancelText = cancelText,
onSubmitClick = onSaveClick,
onCancelClick = onDiscardClick,
onDismiss = onDismiss, onDismiss = onDismiss,
) )
@ -34,7 +40,8 @@ fun SaveChangesDialog(
@Composable @Composable
internal fun SaveChangesDialogPreview() = ElementPreview { internal fun SaveChangesDialogPreview() = ElementPreview {
SaveChangesDialog( SaveChangesDialog(
onSubmitClick = {}, onSaveClick = {},
onDiscardClick = {},
onDismiss = {} onDismiss = {}
) )
} }

View file

@ -38,6 +38,7 @@
<string name="notification_room_invite_body_with_sender">"%1$s invited you to join the room"</string> <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_me">"Me"</string>
<string name="notification_sender_mention_reply">"%1$s mentioned or replied"</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_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_thread_in_room">"Thread in %1$s"</string>
<string name="notification_ticker_text_dm">"%1$s: %2$s"</string> <string name="notification_ticker_text_dm">"%1$s: %2$s"</string>

View file

@ -392,6 +392,7 @@ Are you sure you want to continue?"</string>
<string name="dialog_title_error">"Error"</string> <string name="dialog_title_error">"Error"</string>
<string name="dialog_title_success">"Success"</string> <string name="dialog_title_success">"Success"</string>
<string name="dialog_title_warning">"Warning"</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_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_unsaved_changes_title">"Save changes?"</string>
<string name="dialog_video_quality_selector_subtitle_file_size">"The max file size allowed is: %1$s"</string> <string name="dialog_video_quality_selector_subtitle_file_size">"The max file size allowed is: %1$s"</string>

View file

@ -10,34 +10,33 @@ package io.element.android.tests.testutils
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasClickAction import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import org.junit.rules.TestRule 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) val text = activity.getString(res)
onNode(hasText(text) and hasClickAction()) onNode(
hasText(text) and hasClickAction() and if (inDialog) hasAnyAncestor(isDialog()) else trueMatcher
)
.performClick() .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. * Press the back button in the app bar.
*/ */

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:657d1e0eff5254eb3d36a64212cca38ec129c240cbd61b8cfdeb8399a00a5251 oid sha256:2d60cf2028add0c159867b252f429ec92575b0ee4e89088046ecfbd3c58997df
size 39103 size 38027

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3407623ce83d0c3f710ea45d693823d6125b1bc625c34e2922916110d8a2a442 oid sha256:223002dda410268fbe0c3305ed08749c7fcaabf4d5c3803503525811da2a2841
size 36717 size 35821

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f2a443d3d6733e4fb9c618c4a689c8fb95195f47d3b08513a955f18251028b2f oid sha256:9b6869e6026df038d739cc14a0a585563d595b1430ba23a985ec57680506ead2
size 34203 size 29857

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b2166579c462025a6bff38b05da7574502f7200d5f0a4949b31ea4e4fbc25a67 oid sha256:2b3eb9e8908deb84153b19d3735dd2338fac2a7998793915eeec32d7607a0b94
size 32467 size 28671

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8301124f0464ecd17016d93160899f6a4f3d4bb14ebd8dcd4bc3c3bd77e41996 oid sha256:6109391197169d423341ba95d8f3de581b6b9663f8a017c0b727bf7d9181b228
size 58980 size 56993

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:be70d554880dec4eb3816360dbdbb6b678f2300afc10c91e741418cfac93cfb4 oid sha256:71a4998d420d5cad0a799db7f51223393cca34218a1a23520606667ea320331b
size 56852 size 55323

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:25d60c4025a15e283978fb15abf8417bc896714ebc1faf198a3735978bbbfb18 oid sha256:e3d196d9b4946165718d8a4ad37869d1d81c6134e94e05dcc1b23b71338482cb
size 32392 size 28941

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:29cea35b4105715561f34e4c6fe80a57272444b34b01bd9dd1f0adec67ea8878 oid sha256:b9187a1e5fc63707a2c1a9804411e15439469f8562eac2c9766a4e033e214166
size 30270 size 27289

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4b1ce86ad21d9ccd5b170a1628985eadbc5ac92be521ab55e9a1b22eb48c1476 oid sha256:72be11ed273a790b0ab8e5023c1e21384c020e1b0fc0e3950de8f041c4004e64
size 32552 size 33758

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:da39e0496083394c522d40ed4cc62edc614ddca54ccfb05d5b7c68cfb030960a oid sha256:c1cbcce698faad80969cd00d2680afd7cd61b6ba649f0b67e8c7fb2fe0dbe746
size 34559 size 35884

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9d89a6c9a49b2b0e4fa37e219cec8e0dbe90ed8ca6e71f0ec5e8788076f91526 oid sha256:9849f4591df3218c78c91e6731a864b72abaa699caa6321c818197faaf6b85af
size 23679 size 18326

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e496c8d67a678a7d4c729a3368c145afa080252dd68708b1ba95d98706cd614c oid sha256:d47c650eae62a550c80aeed85775062304132debf33e5279fddc74c787ad40b3
size 22265 size 17211