Remove confirmExitAction and use AsyncAction.ConfirmingCancellation instead.
This commit is contained in:
parent
61b7ee03c9
commit
c97e60fcaf
15 changed files with 102 additions and 45 deletions
|
|
@ -383,7 +383,16 @@ class RoomDetailsFlowNode(
|
|||
knockRequestsListEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
NavTarget.SecurityAndPrivacy -> {
|
||||
securityAndPrivacyEntryPoint.createNode(this, buildContext)
|
||||
val callback = object : SecurityAndPrivacyEntryPoint.Callback {
|
||||
override fun onDone() {
|
||||
backstack.pop()
|
||||
}
|
||||
}
|
||||
securityAndPrivacyEntryPoint.createNode(
|
||||
parentNode = this,
|
||||
buildContext = buildContext,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
is NavTarget.VerifyUser -> {
|
||||
val params = OutgoingVerificationEntryPoint.Params(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,19 @@
|
|||
|
||||
package io.element.android.features.securityandprivacy.api
|
||||
|
||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
|
||||
fun interface SecurityAndPrivacyEntryPoint : SimpleFeatureEntryPoint
|
||||
fun interface SecurityAndPrivacyEntryPoint : FeatureEntryPoint {
|
||||
interface Callback : Plugin {
|
||||
fun onDone()
|
||||
}
|
||||
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: Callback,
|
||||
): Node
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ import io.element.android.libraries.di.RoomScope
|
|||
|
||||
@ContributesBinding(RoomScope::class)
|
||||
class DefaultSecurityAndPrivacyEntryPoint : SecurityAndPrivacyEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<SecurityAndPrivacyFlowNode>(buildContext)
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: SecurityAndPrivacyEntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<SecurityAndPrivacyFlowNode>(buildContext, listOf(callback))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ import com.bumble.appyx.navmodel.backstack.BackStack
|
|||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
|
||||
import io.element.android.features.securityandprivacy.impl.editroomaddress.EditRoomAddressNode
|
||||
import io.element.android.features.securityandprivacy.impl.root.SecurityAndPrivacyNode
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.callback
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
|
@ -47,7 +49,8 @@ class SecurityAndPrivacyFlowNode(
|
|||
data object EditRoomAddress : NavTarget
|
||||
}
|
||||
|
||||
private val navigator = BackstackSecurityAndPrivacyNavigator(backstack)
|
||||
private val callback: SecurityAndPrivacyEntryPoint.Callback = callback()
|
||||
private val navigator = BackstackSecurityAndPrivacyNavigator(callback, backstack)
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
|
|
|
|||
|
|
@ -12,15 +12,22 @@ import com.bumble.appyx.core.plugin.Plugin
|
|||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntryPoint
|
||||
|
||||
interface SecurityAndPrivacyNavigator : Plugin {
|
||||
fun onDone()
|
||||
fun openEditRoomAddress()
|
||||
fun closeEditRoomAddress()
|
||||
}
|
||||
|
||||
class BackstackSecurityAndPrivacyNavigator(
|
||||
private val callback: SecurityAndPrivacyEntryPoint.Callback,
|
||||
private val backStack: BackStack<SecurityAndPrivacyFlowNode.NavTarget>
|
||||
) : SecurityAndPrivacyNavigator {
|
||||
override fun onDone() {
|
||||
callback.onDone()
|
||||
}
|
||||
|
||||
override fun openEditRoomAddress() {
|
||||
backStack.push(SecurityAndPrivacyFlowNode.NavTarget.EditRoomAddress)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ class SecurityAndPrivacyNode(
|
|||
val state by stateFlow.collectAsState()
|
||||
SecurityAndPrivacyView(
|
||||
state = state,
|
||||
onBackClick = this::navigateUp,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ class SecurityAndPrivacyPresenter(
|
|||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
|
||||
}.collectAsState(false)
|
||||
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
var confirmExitAction by remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
val roomInfo by room.roomInfoFlow.collectAsState()
|
||||
|
|
@ -150,18 +149,24 @@ class SecurityAndPrivacyPresenter(
|
|||
saveAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
SecurityAndPrivacyEvents.Exit -> {
|
||||
confirmExitAction = if (savedSettings == editedSettings || confirmExitAction.isConfirming()) {
|
||||
saveAction.value = if (savedSettings == editedSettings || saveAction.value == AsyncAction.ConfirmingCancellation) {
|
||||
AsyncAction.Success(Unit)
|
||||
} else {
|
||||
AsyncAction.ConfirmingNoParams
|
||||
AsyncAction.ConfirmingCancellation
|
||||
}
|
||||
}
|
||||
SecurityAndPrivacyEvents.DismissExitConfirmation -> {
|
||||
confirmExitAction = AsyncAction.Uninitialized
|
||||
saveAction.value = AsyncAction.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(saveAction.value.isSuccess()) {
|
||||
if (saveAction.value.isSuccess()) {
|
||||
navigator.onDone()
|
||||
}
|
||||
}
|
||||
|
||||
val state = SecurityAndPrivacyState(
|
||||
savedSettings = savedSettings,
|
||||
editedSettings = editedSettings,
|
||||
|
|
@ -171,7 +176,6 @@ class SecurityAndPrivacyPresenter(
|
|||
saveAction = saveAction.value,
|
||||
permissions = permissions,
|
||||
isSpace = roomInfo.isSpace,
|
||||
confirmExitAction = confirmExitAction,
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ data class SecurityAndPrivacyState(
|
|||
val showEnableEncryptionConfirmation: Boolean,
|
||||
val isKnockEnabled: Boolean,
|
||||
val saveAction: AsyncAction<Unit>,
|
||||
val confirmExitAction: AsyncAction<Unit>,
|
||||
val isSpace: Boolean,
|
||||
private val permissions: SecurityAndPrivacyPermissions,
|
||||
val eventSink: (SecurityAndPrivacyEvents) -> Unit
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAn
|
|||
isSpace = false,
|
||||
),
|
||||
aSecurityAndPrivacyState(
|
||||
confirmExitAction = AsyncAction.ConfirmingCancellation,
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
isSpace = false,
|
||||
),
|
||||
aSecurityAndPrivacyState(
|
||||
|
|
@ -109,7 +109,6 @@ fun aSecurityAndPrivacyState(
|
|||
homeserverName: String = "myserver.xyz",
|
||||
showEncryptionConfirmation: Boolean = false,
|
||||
saveAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
confirmExitAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
permissions: SecurityAndPrivacyPermissions = SecurityAndPrivacyPermissions(
|
||||
canChangeRoomAccess = true,
|
||||
canChangeHistoryVisibility = true,
|
||||
|
|
@ -125,7 +124,6 @@ fun aSecurityAndPrivacyState(
|
|||
homeserverName = homeserverName,
|
||||
showEnableEncryptionConfirmation = showEncryptionConfirmation,
|
||||
saveAction = saveAction,
|
||||
confirmExitAction = confirmExitAction,
|
||||
isKnockEnabled = isKnockEnabled,
|
||||
permissions = permissions,
|
||||
isSpace = isSpace,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp
|
|||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.securityandprivacy.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
|
|
@ -56,7 +57,6 @@ import kotlinx.collections.immutable.ImmutableSet
|
|||
@Composable
|
||||
fun SecurityAndPrivacyView(
|
||||
state: SecurityAndPrivacyState,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler {
|
||||
|
|
@ -130,6 +130,16 @@ fun SecurityAndPrivacyView(
|
|||
async = state.saveAction,
|
||||
onSuccess = { },
|
||||
onErrorDismiss = { state.eventSink(SecurityAndPrivacyEvents.DismissSaveError) },
|
||||
confirmationDialog = { confirming ->
|
||||
when (confirming) {
|
||||
is AsyncAction.ConfirmingCancellation ->
|
||||
SaveChangesDialog(
|
||||
onSaveClick = { state.eventSink(SecurityAndPrivacyEvents.Save) },
|
||||
onDiscardClick = { state.eventSink(SecurityAndPrivacyEvents.Exit) },
|
||||
onDismiss = { state.eventSink(SecurityAndPrivacyEvents.DismissExitConfirmation) }
|
||||
)
|
||||
}
|
||||
},
|
||||
errorMessage = { stringResource(CommonStrings.error_unknown) },
|
||||
progressDialog = {
|
||||
AsyncActionViewDefaults.ProgressDialog(
|
||||
|
|
@ -138,18 +148,6 @@ fun SecurityAndPrivacyView(
|
|||
},
|
||||
onRetry = { state.eventSink(SecurityAndPrivacyEvents.Save) },
|
||||
)
|
||||
AsyncActionView(
|
||||
async = state.confirmExitAction,
|
||||
onSuccess = { onBackClick() },
|
||||
onErrorDismiss = { },
|
||||
confirmationDialog = {
|
||||
SaveChangesDialog(
|
||||
onSaveClick = { state.eventSink(SecurityAndPrivacyEvents.Save) },
|
||||
onDiscardClick = { state.eventSink(SecurityAndPrivacyEvents.Exit) },
|
||||
onDismiss = { state.eventSink(SecurityAndPrivacyEvents.DismissExitConfirmation) }
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -426,6 +424,5 @@ internal fun SecurityAndPrivacyViewDarkPreview(@PreviewParameter(SecurityAndPriv
|
|||
private fun ContentToPreview(state: SecurityAndPrivacyState) {
|
||||
SecurityAndPrivacyView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,14 @@ package io.element.android.features.securityandprivacy.impl
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeSecurityAndPrivacyNavigator(
|
||||
private val onDoneLambda: () -> Unit = { lambdaError() },
|
||||
private val openEditRoomAddressLambda: () -> Unit = { lambdaError() },
|
||||
private val closeEditRoomAddressLambda: () -> Unit = { lambdaError() },
|
||||
) : SecurityAndPrivacyNavigator {
|
||||
override fun onDone() {
|
||||
onDoneLambda()
|
||||
}
|
||||
|
||||
override fun openEditRoomAddress() {
|
||||
openEditRoomAddressLambda()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ class SecurityAndPrivacyPresenterTest {
|
|||
@Test
|
||||
fun `present - edit room address`() = runTest {
|
||||
val openEditRoomAddressLambda = lambdaRecorder<Unit> { }
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda)
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(openEditRoomAddressLambda = openEditRoomAddressLambda)
|
||||
val presenter = createSecurityAndPrivacyPresenter(navigator = navigator)
|
||||
presenter.test {
|
||||
skipItems(1)
|
||||
|
|
@ -231,7 +231,14 @@ class SecurityAndPrivacyPresenterTest {
|
|||
updateRoomVisibilityResult = updateRoomVisibilityLambda,
|
||||
updateRoomHistoryVisibilityResult = updateRoomHistoryVisibilityLambda,
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||
val onDoneLambda = lambdaRecorder<Unit> { }
|
||||
val navigator = FakeSecurityAndPrivacyNavigator(
|
||||
onDoneLambda = onDoneLambda,
|
||||
)
|
||||
val presenter = createSecurityAndPrivacyPresenter(
|
||||
room = room,
|
||||
navigator = navigator,
|
||||
)
|
||||
presenter.test {
|
||||
skipItems(2)
|
||||
with(awaitItem()) {
|
||||
|
|
@ -276,6 +283,7 @@ class SecurityAndPrivacyPresenterTest {
|
|||
assert(updateJoinRuleLambda).isCalledOnce()
|
||||
assert(updateRoomVisibilityLambda).isCalledOnce()
|
||||
assert(updateRoomHistoryVisibilityLambda).isCalledOnce()
|
||||
onDoneLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import io.element.android.features.securityandprivacy.impl.root.aSecurityAndPriv
|
|||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
|
|
@ -50,27 +49,27 @@ class SecurityAndPrivacyViewTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `confirm cancellation emits the expected event`() {
|
||||
fun `discard cancellation emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
confirmExitAction = AsyncAction.ConfirmingCancellation,
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = recorder,
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(CommonStrings.action_ok)
|
||||
rule.clickOn(CommonStrings.action_discard)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.Exit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dismiss cancellation confirmation emits the expected event`() {
|
||||
fun `save cancellation confirmation emits the expected event`() {
|
||||
val recorder = EventsRecorder<SecurityAndPrivacyEvents>()
|
||||
val state = aSecurityAndPrivacyState(
|
||||
confirmExitAction = AsyncAction.ConfirmingCancellation,
|
||||
saveAction = AsyncAction.ConfirmingCancellation,
|
||||
eventSink = recorder,
|
||||
)
|
||||
rule.setSecurityAndPrivacyView(state)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.DismissExitConfirmation)
|
||||
rule.clickOn(CommonStrings.action_save, inDialog = true)
|
||||
recorder.assertSingle(SecurityAndPrivacyEvents.Save)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -185,12 +184,10 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSecur
|
|||
state: SecurityAndPrivacyState = aSecurityAndPrivacyState(
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
SecurityAndPrivacyView(
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ import io.element.android.features.securityandprivacy.api.SecurityAndPrivacyEntr
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeSecurityAndPrivacyEntryPoint : SecurityAndPrivacyEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: SecurityAndPrivacyEntryPoint.Callback,
|
||||
): Node {
|
||||
lambdaError()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,14 @@ package io.element.android.tests.testutils
|
|||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.hasTestTag
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.onFirst
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
|
|
@ -22,9 +25,16 @@ import androidx.compose.ui.test.performClick
|
|||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import org.junit.rules.TestRule
|
||||
|
||||
fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.clickOn(@StringRes res: Int) {
|
||||
val trueMatcher = SemanticsMatcher("true matcher") { true }
|
||||
|
||||
fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.clickOn(
|
||||
@StringRes res: Int,
|
||||
inDialog: Boolean = false,
|
||||
) {
|
||||
val text = activity.getString(res)
|
||||
onNode(hasText(text) and hasClickAction())
|
||||
onNode(
|
||||
hasText(text) and hasClickAction() and if (inDialog) hasAnyAncestor(isDialog()) else trueMatcher
|
||||
)
|
||||
.performClick()
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue