Merge pull request #1737 from vector-im/feature/bma/improvePages

Create UserStoryFlowPage
This commit is contained in:
Benoit Marty 2023-11-07 14:30:38 +01:00 committed by GitHub
commit e2870442c7
9 changed files with 313 additions and 360 deletions

View file

@ -17,19 +17,16 @@
package io.element.android.features.lockscreen.impl.setup.biometric package io.element.android.features.lockscreen.impl.setup.biometric
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Fingerprint import androidx.compose.material.icons.filled.Fingerprint
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.lockscreen.impl.R import io.element.android.features.lockscreen.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -76,10 +73,8 @@ private fun SetupBiometricFooter(
onSkipClicked: () -> Unit, onSkipClicked: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Column( ButtonColumnMolecule(
modifier = modifier.fillMaxWidth(), modifier = modifier,
verticalArrangement = spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication) val biometricAuth = stringResource(id = R.string.screen_app_lock_biometric_authentication)
Button( Button(

View file

@ -18,9 +18,9 @@ package io.element.android.features.logout.impl
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -29,11 +29,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialog
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.ConfirmationDialog
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
@ -41,7 +38,6 @@ import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor import io.element.android.libraries.designsystem.theme.progressIndicatorTrackColor
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.BackupUploadState
@ -51,7 +47,6 @@ import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LogoutView( fun LogoutView(
state: LogoutState, state: LogoutState,
@ -62,29 +57,23 @@ fun LogoutView(
) { ) {
val eventSink = state.eventSink val eventSink = state.eventSink
HeaderFooterPage( FlowStepPage(
onBackClicked = onBackClicked,
title = title(state),
subTitle = subtitle(state),
iconResourceId = CommonDrawables.ic_key,
modifier = modifier, modifier = modifier,
topBar = { content = { Content(state) },
TopAppBar( buttons = {
navigationIcon = { BackButton(onClick = onBackClicked) }, Buttons(
title = {},
)
},
header = {
HeaderContent(state = state)
},
footer = {
BottomMenu(
state = state, state = state,
onChangeRecoveryKeyClicked = onChangeRecoveryKeyClicked, onChangeRecoveryKeyClicked = onChangeRecoveryKeyClicked,
onLogoutClicked = { onLogoutClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = false)) eventSink(LogoutEvents.Logout(ignoreSdkError = false))
}, }
) )
} },
) { )
Content(state = state)
}
// Log out confirmation dialog // Log out confirmation dialog
if (state.showConfirmationDialog) { if (state.showConfirmationDialog) {
@ -92,9 +81,6 @@ fun LogoutView(
title = stringResource(id = CommonStrings.action_signout), title = stringResource(id = CommonStrings.action_signout),
content = stringResource(id = R.string.screen_signout_confirmation_dialog_content), content = stringResource(id = R.string.screen_signout_confirmation_dialog_content),
submitText = stringResource(id = CommonStrings.action_signout), submitText = stringResource(id = CommonStrings.action_signout),
onCancelClicked = {
eventSink(LogoutEvents.CloseDialogs)
},
onSubmitClicked = { onSubmitClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = false)) eventSink(LogoutEvents.Logout(ignoreSdkError = false))
}, },
@ -112,9 +98,6 @@ fun LogoutView(
title = stringResource(id = CommonStrings.dialog_title_error), title = stringResource(id = CommonStrings.dialog_title_error),
content = stringResource(id = CommonStrings.error_unknown), content = stringResource(id = CommonStrings.error_unknown),
submitText = stringResource(id = CommonStrings.action_signout_anyway), submitText = stringResource(id = CommonStrings.action_signout_anyway),
onCancelClicked = {
eventSink(LogoutEvents.CloseDialogs)
},
onSubmitClicked = { onSubmitClicked = {
eventSink(LogoutEvents.Logout(ignoreSdkError = true)) eventSink(LogoutEvents.Logout(ignoreSdkError = true))
}, },
@ -132,30 +115,23 @@ fun LogoutView(
} }
@Composable @Composable
private fun HeaderContent( private fun title(state: LogoutState): String {
state: LogoutState, return when {
modifier: Modifier = Modifier,
) {
val title = when {
state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_title) state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_title)
state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_title) state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_title)
else -> stringResource(CommonStrings.action_signout) else -> stringResource(CommonStrings.action_signout)
} }
val subtitle = when { }
@Composable
private fun subtitle(state: LogoutState): String? {
return when {
(state.backupUploadState as? BackupUploadState.SteadyException)?.exception is SteadyStateException.Connection -> (state.backupUploadState as? BackupUploadState.SteadyException)?.exception is SteadyStateException.Connection ->
stringResource(id = R.string.screen_signout_key_backup_offline_subtitle) stringResource(id = R.string.screen_signout_key_backup_offline_subtitle)
state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_subtitle) state.backupUploadState.isBackingUp() -> stringResource(id = R.string.screen_signout_key_backup_ongoing_subtitle)
state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle) state.isLastSession -> stringResource(id = R.string.screen_signout_key_backup_disabled_subtitle)
else -> null else -> null
} }
val paddingTop = 0.dp
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = paddingTop),
iconResourceId = CommonDrawables.ic_key,
title = title,
subTitle = subtitle,
)
} }
private fun BackupUploadState.isBackingUp(): Boolean { private fun BackupUploadState.isBackingUp(): Boolean {
@ -171,37 +147,33 @@ private fun BackupUploadState.isBackingUp(): Boolean {
} }
@Composable @Composable
private fun BottomMenu( private fun ColumnScope.Buttons(
state: LogoutState, state: LogoutState,
onLogoutClicked: () -> Unit, onLogoutClicked: () -> Unit,
onChangeRecoveryKeyClicked: () -> Unit, onChangeRecoveryKeyClicked: () -> Unit,
) { ) {
val logoutAction = state.logoutAction val logoutAction = state.logoutAction
ButtonColumnMolecule( if (state.isLastSession) {
modifier = Modifier.padding(bottom = 20.dp) OutlinedButton(
) { text = stringResource(id = CommonStrings.common_settings),
if (state.isLastSession) { modifier = Modifier.fillMaxWidth(),
OutlinedButton( onClick = onChangeRecoveryKeyClicked,
text = stringResource(id = CommonStrings.common_settings),
modifier = Modifier.fillMaxWidth(),
onClick = onChangeRecoveryKeyClicked,
)
}
val signOutSubmitRes = when {
logoutAction is Async.Loading -> R.string.screen_signout_in_progress_dialog_content
state.backupUploadState.isBackingUp() -> CommonStrings.action_signout_anyway
else -> CommonStrings.action_signout
}
Button(
text = stringResource(id = signOutSubmitRes),
showProgress = logoutAction is Async.Loading,
destructive = true,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.signOut),
onClick = onLogoutClicked,
) )
} }
val signOutSubmitRes = when {
logoutAction is Async.Loading -> R.string.screen_signout_in_progress_dialog_content
state.backupUploadState.isBackingUp() -> CommonStrings.action_signout_anyway
else -> CommonStrings.action_signout
}
Button(
text = stringResource(id = signOutSubmitRes),
showProgress = logoutAction is Async.Loading,
destructive = true,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.signOut),
onClick = onLogoutClicked,
)
} }
@Composable @Composable

View file

@ -18,11 +18,11 @@ package io.element.android.features.securebackup.impl.disable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -31,10 +31,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.securebackup.impl.R import io.element.android.features.securebackup.impl.R
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
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.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -42,11 +39,9 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SecureBackupDisableView( fun SecureBackupDisableView(
state: SecureBackupDisableState, state: SecureBackupDisableState,
@ -59,23 +54,16 @@ fun SecureBackupDisableView(
onDone() onDone()
} }
} }
HeaderFooterPage( FlowStepPage(
modifier = modifier, modifier = modifier,
topBar = { onBackClicked = onBackClicked,
TopAppBar( title = stringResource(id = R.string.screen_key_backup_disable_title),
navigationIcon = { BackButton(onClick = onBackClicked) }, subTitle = stringResource(id = R.string.screen_key_backup_disable_description),
title = {}, iconResourceId = CommonDrawables.ic_key_off,
) content = { Content(state = state) },
}, buttons = { Buttons(state = state) },
header = { )
HeaderContent()
},
footer = {
BottomMenu(state = state)
}
) {
Content(state = state)
}
if (state.showConfirmationDialog) { if (state.showConfirmationDialog) {
SecureBackupDisableConfirmationDialog( SecureBackupDisableConfirmationDialog(
onConfirm = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup(force = true)) }, onConfirm = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup(force = true)) },
@ -102,32 +90,16 @@ private fun SecureBackupDisableConfirmationDialog(onConfirm: () -> Unit, onDismi
} }
@Composable @Composable
private fun HeaderContent( private fun ColumnScope.Buttons(
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 0.dp),
iconResourceId = CommonDrawables.ic_key_off,
title = stringResource(id = R.string.screen_key_backup_disable_title),
subTitle = stringResource(id = R.string.screen_key_backup_disable_description),
)
}
@Composable
private fun BottomMenu(
state: SecureBackupDisableState, state: SecureBackupDisableState,
) { ) {
ButtonColumnMolecule( Button(
modifier = Modifier.padding(bottom = 20.dp) text = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable),
) { showProgress = state.disableAction.isLoading(),
Button( destructive = true,
text = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable), modifier = Modifier.fillMaxWidth(),
showProgress = state.disableAction.isLoading(), onClick = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup(force = false)) }
destructive = true, )
modifier = Modifier.fillMaxWidth(),
onClick = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup(force = false)) }
)
}
} }
@Composable @Composable

View file

@ -16,29 +16,22 @@
package io.element.android.features.securebackup.impl.enable package io.element.android.features.securebackup.impl.enable
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier 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 androidx.compose.ui.unit.dp
import io.element.android.features.securebackup.impl.R import io.element.android.features.securebackup.impl.R
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
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
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SecureBackupEnableView( fun SecureBackupEnableView(
state: SecureBackupEnableState, state: SecureBackupEnableState,
@ -51,20 +44,12 @@ fun SecureBackupEnableView(
onDone() onDone()
} }
} }
HeaderFooterPage( FlowStepPage(
modifier = modifier, modifier = modifier,
topBar = { onBackClicked = onBackClicked,
TopAppBar( title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
navigationIcon = { BackButton(onClick = onBackClicked) }, iconResourceId = CommonDrawables.ic_key,
title = {}, buttons = { Buttons(state = state) }
)
},
header = {
HeaderContent()
},
footer = {
BottomMenu(state = state)
}
) )
if (state.enableAction is Async.Failure) { if (state.enableAction is Async.Failure) {
ErrorDialog( ErrorDialog(
@ -75,31 +60,15 @@ fun SecureBackupEnableView(
} }
@Composable @Composable
private fun HeaderContent( private fun ColumnScope.Buttons(
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 0.dp),
iconResourceId = CommonDrawables.ic_key,
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
subTitle = null,
)
}
@Composable
private fun BottomMenu(
state: SecureBackupEnableState, state: SecureBackupEnableState,
) { ) {
ButtonColumnMolecule( Button(
modifier = Modifier.padding(bottom = 20.dp) text = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
) { showProgress = state.enableAction.isLoading(),
Button( modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable), onClick = { state.eventSink.invoke(SecureBackupEnableEvents.EnableBackup) }
showProgress = state.enableAction.isLoading(), )
modifier = Modifier.fillMaxWidth(),
onClick = { state.eventSink.invoke(SecureBackupEnableEvents.EnableBackup) }
)
}
} }
@PreviewsDayNight @PreviewsDayNight

View file

@ -16,9 +16,9 @@
package io.element.android.features.securebackup.impl.enter package io.element.android.features.securebackup.impl.enter
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -26,19 +26,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.securebackup.impl.R import io.element.android.features.securebackup.impl.R
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.async.AsyncView import io.element.android.libraries.designsystem.components.async.AsyncView
import io.element.android.libraries.designsystem.components.button.BackButton
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
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SecureBackupEnterRecoveryKeyView( fun SecureBackupEnterRecoveryKeyView(
state: SecureBackupEnterRecoveryKeyState, state: SecureBackupEnterRecoveryKeyState,
@ -53,79 +48,46 @@ fun SecureBackupEnterRecoveryKeyView(
onErrorDismiss = { state.eventSink(SecureBackupEnterRecoveryKeyEvents.ClearDialog) }, onErrorDismiss = { state.eventSink(SecureBackupEnterRecoveryKeyEvents.ClearDialog) },
) )
HeaderFooterPage( FlowStepPage(
modifier = modifier, modifier = modifier,
topBar = { onBackClicked = onBackClicked,
TopAppBar(
navigationIcon = { BackButton(onClick = onBackClicked) },
title = {},
)
},
header = {
HeaderContent()
},
footer = {
BottomMenu(
state = state,
onSubmit = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
},
)
}
) {
Content(
state = state,
onChange = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange(it))
},
onSubmit = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
})
}
}
@Composable
private fun HeaderContent(
modifier: Modifier = Modifier,
) {
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 0.dp),
iconResourceId = CommonDrawables.ic_key, iconResourceId = CommonDrawables.ic_key,
title = stringResource(id = R.string.screen_recovery_key_confirm_title), title = stringResource(id = R.string.screen_recovery_key_confirm_title),
subTitle = stringResource(id = R.string.screen_recovery_key_confirm_description), subTitle = stringResource(id = R.string.screen_recovery_key_confirm_description),
content = { Content(state = state) },
buttons = { Buttons(state = state) }
) )
} }
@Composable
private fun BottomMenu(
state: SecureBackupEnterRecoveryKeyState,
onSubmit: () -> Unit,
) {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 20.dp)
) {
Button(
text = stringResource(id = CommonStrings.action_confirm),
enabled = state.isSubmitEnabled,
showProgress = state.submitAction.isLoading(),
modifier = Modifier.fillMaxWidth(),
onClick = onSubmit
)
}
}
@Composable @Composable
private fun Content( private fun Content(
state: SecureBackupEnterRecoveryKeyState, state: SecureBackupEnterRecoveryKeyState,
onChange: (String) -> Unit,
onSubmit: () -> Unit,
) { ) {
RecoveryKeyView( RecoveryKeyView(
modifier = Modifier.padding(top = 52.dp), modifier = Modifier.padding(top = 52.dp),
state = state.recoveryKeyViewState, state = state.recoveryKeyViewState,
onClick = null, onClick = null,
onChange = onChange, onChange = {
onSubmit = onSubmit, state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange(it))
},
onSubmit = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
},
)
}
@Composable
private fun ColumnScope.Buttons(
state: SecureBackupEnterRecoveryKeyState,
) {
Button(
text = stringResource(id = CommonStrings.action_confirm),
enabled = state.isSubmitEnabled,
showProgress = state.submitAction.isLoading(),
modifier = Modifier.fillMaxWidth(),
onClick = {
state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit)
}
) )
} }

View file

@ -16,10 +16,9 @@
package io.element.android.features.securebackup.impl.setup package io.element.android.features.securebackup.impl.setup
import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -28,24 +27,18 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.securebackup.impl.R import io.element.android.features.securebackup.impl.R
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
import io.element.android.libraries.androidutils.system.copyToClipboard import io.element.android.libraries.androidutils.system.copyToClipboard
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
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.ConfirmationDialog
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
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SecureBackupSetupView( fun SecureBackupSetupView(
state: SecureBackupSetupState, state: SecureBackupSetupState,
@ -53,68 +46,15 @@ fun SecureBackupSetupView(
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current FlowStepPage(
val canGoBack = state.canGoBack()
BackHandler(enabled = canGoBack) {
onBackClicked()
}
HeaderFooterPage(
modifier = modifier, modifier = modifier,
topBar = { onBackClicked = onBackClicked.takeIf { state.canGoBack() },
TopAppBar( title = title(state),
navigationIcon = { subTitle = subtitle(state),
if (canGoBack) { iconResourceId = CommonDrawables.ic_key,
BackButton(onClick = onBackClicked) content = { Content(state) },
} buttons = { Buttons(state, onDone = onDone) },
}, )
title = {},
)
},
header = {
HeaderContent(state = state)
},
footer = {
val chooserTitle = stringResource(id = R.string.screen_recovery_key_save_action)
BottomMenu(
state = state,
onSaveClicked = { key ->
context.startSharePlainTextIntent(
activityResultLauncher = null,
chooserTitle = chooserTitle,
text = key,
)
state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved)
},
onDone = {
if (state.setupState is SetupState.CreatedAndSaved) {
onDone()
} else {
state.eventSink.invoke(SecureBackupSetupEvents.Done)
}
},
)
}
) {
val formattedRecoveryKey = state.recoveryKeyViewState.formattedRecoveryKey
val clickLambda = if (formattedRecoveryKey != null) {
{
context.copyToClipboard(
formattedRecoveryKey,
context.getString(R.string.screen_recovery_key_copied_to_clipboard)
)
state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved)
}
} else {
if (!state.recoveryKeyViewState.inProgress) {
{
state.eventSink.invoke(SecureBackupSetupEvents.CreateRecoveryKey)
}
} else {
null
}
}
Content(state = state.recoveryKeyViewState, onClick = clickLambda)
}
if (state.showSaveConfirmationDialog) { if (state.showSaveConfirmationDialog) {
ConfirmationDialog( ConfirmationDialog(
@ -134,12 +74,8 @@ private fun SecureBackupSetupState.canGoBack(): Boolean {
} }
@Composable @Composable
private fun HeaderContent( private fun title(state: SecureBackupSetupState): String {
state: SecureBackupSetupState, return when (state.setupState) {
modifier: Modifier = Modifier,
) {
val setupState = state.setupState
val title = when (setupState) {
SetupState.Init, SetupState.Init,
SetupState.Creating -> if (state.isChangeRecoveryKeyUserStory) SetupState.Creating -> if (state.isChangeRecoveryKeyUserStory)
stringResource(id = R.string.screen_recovery_key_change_title) stringResource(id = R.string.screen_recovery_key_change_title)
@ -149,7 +85,11 @@ private fun HeaderContent(
is SetupState.CreatedAndSaved -> is SetupState.CreatedAndSaved ->
stringResource(id = R.string.screen_recovery_key_save_title) stringResource(id = R.string.screen_recovery_key_save_title)
} }
val subTitle = when (setupState) { }
@Composable
private fun subtitle(state: SecureBackupSetupState): String {
return when (state.setupState) {
SetupState.Init, SetupState.Init,
SetupState.Creating -> if (state.isChangeRecoveryKeyUserStory) SetupState.Creating -> if (state.isChangeRecoveryKeyUserStory)
stringResource(id = R.string.screen_recovery_key_change_description) stringResource(id = R.string.screen_recovery_key_change_description)
@ -159,67 +99,87 @@ private fun HeaderContent(
is SetupState.CreatedAndSaved -> is SetupState.CreatedAndSaved ->
stringResource(id = R.string.screen_recovery_key_save_description) stringResource(id = R.string.screen_recovery_key_save_description)
} }
IconTitleSubtitleMolecule(
modifier = modifier.padding(top = 0.dp),
iconResourceId = CommonDrawables.ic_key,
title = title,
subTitle = subTitle,
)
}
@Composable
private fun BottomMenu(
state: SecureBackupSetupState,
onSaveClicked: (String) -> Unit,
onDone: () -> Unit,
) {
val setupState = state.setupState
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 20.dp)
) {
when (setupState) {
SetupState.Init,
SetupState.Creating -> {
Button(
text = stringResource(id = CommonStrings.action_done),
enabled = false,
modifier = Modifier.fillMaxWidth(),
onClick = onDone
)
}
is SetupState.Created,
is SetupState.CreatedAndSaved -> {
OutlinedButton(
text = stringResource(id = R.string.screen_recovery_key_save_action),
leadingIcon = IconSource.Resource(CommonDrawables.ic_compound_download),
modifier = Modifier.fillMaxWidth(),
onClick = { onSaveClicked(setupState.recoveryKey()!!) },
)
Button(
text = stringResource(id = CommonStrings.action_done),
modifier = Modifier.fillMaxWidth(),
onClick = onDone,
)
}
}
}
} }
@Composable @Composable
private fun Content( private fun Content(
state: RecoveryKeyViewState, state: SecureBackupSetupState,
onClick: (() -> Unit)?,
) { ) {
val modifier = Modifier.padding(top = 52.dp) val context = LocalContext.current
val formattedRecoveryKey = state.recoveryKeyViewState.formattedRecoveryKey
val clickLambda = if (formattedRecoveryKey != null) {
{
context.copyToClipboard(
formattedRecoveryKey,
context.getString(R.string.screen_recovery_key_copied_to_clipboard)
)
state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved)
}
} else {
if (!state.recoveryKeyViewState.inProgress) {
{
state.eventSink.invoke(SecureBackupSetupEvents.CreateRecoveryKey)
}
} else {
null
}
}
RecoveryKeyView( RecoveryKeyView(
modifier = modifier, modifier = Modifier.padding(top = 52.dp),
state = state, state = state.recoveryKeyViewState,
onClick = onClick, onClick = clickLambda,
onChange = null, onChange = null,
onSubmit = null, onSubmit = null,
) )
} }
@Composable
private fun ColumnScope.Buttons(
state: SecureBackupSetupState,
onDone: () -> Unit,
) {
val context = LocalContext.current
val chooserTitle = stringResource(id = R.string.screen_recovery_key_save_action)
when (state.setupState) {
SetupState.Init,
SetupState.Creating -> {
Button(
text = stringResource(id = CommonStrings.action_done),
enabled = false,
modifier = Modifier.fillMaxWidth(),
onClick = onDone
)
}
is SetupState.Created,
is SetupState.CreatedAndSaved -> {
OutlinedButton(
text = stringResource(id = R.string.screen_recovery_key_save_action),
leadingIcon = IconSource.Resource(CommonDrawables.ic_compound_download),
modifier = Modifier.fillMaxWidth(),
onClick = {
context.startSharePlainTextIntent(
activityResultLauncher = null,
chooserTitle = chooserTitle,
text = state.setupState.recoveryKey()!!,
)
state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved)
},
)
Button(
text = stringResource(id = CommonStrings.action_done),
modifier = Modifier.fillMaxWidth(),
onClick = {
if (state.setupState is SetupState.CreatedAndSaved) {
onDone()
} else {
state.eventSink.invoke(SecureBackupSetupEvents.Done)
}
},
)
}
}
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun SecureBackupSetupViewPreview( internal fun SecureBackupSetupViewPreview(

View file

@ -0,0 +1,117 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.atomic.pages
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.R
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.theme.ElementTheme
/**
* A Page with:
* - a top bar as TobAppBar with optional back button (displayed if [onBackClicked] is not null)
* - a header, as IconTitleSubtitleMolecule
* - a content.
* - a footer, as ButtonColumnMolecule
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FlowStepPage(
iconResourceId: Int?,
title: String,
modifier: Modifier = Modifier,
onBackClicked: (() -> Unit)? = null,
subTitle: String? = null,
content: @Composable () -> Unit = {},
buttons: @Composable ColumnScope.() -> Unit = {},
) {
BackHandler(enabled = onBackClicked != null) {
onBackClicked?.invoke()
}
HeaderFooterPage(
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = {
if (onBackClicked != null) {
BackButton(onClick = onBackClicked)
}
},
title = {},
)
},
header = {
IconTitleSubtitleMolecule(
iconResourceId = iconResourceId,
title = title,
subTitle = subTitle,
)
},
content = content,
footer = {
ButtonColumnMolecule(
modifier = Modifier.padding(bottom = 20.dp)
) {
buttons()
}
}
)
}
@PreviewsDayNight
@Composable
internal fun FlowStepPagePreview() = ElementPreview {
FlowStepPage(
onBackClicked = {},
title = "Title",
subTitle = "Subtitle",
iconResourceId = R.drawable.ic_compound_computer,
content = {
Box(
Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "Content",
style = ElementTheme.typography.fontHeadingXlBold
)
}
},
buttons = {
TextButton(text = "A button", onClick = { })
Button(text = "Continue", onClick = { })
}
)
}

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:735eecae838f9ea7413d44e33d1ff7b7ce8524a7ee9c601d889f2a23cd06dbee
size 18530

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e1ec13019a59463953135a6b469bcaeed3e61f868efd45c8f7b612a6d96ba711
size 17672