Merge pull request #3750 from element-hq/feature/bma/cryptoIteration
UI iteration on the encryption settings
This commit is contained in:
commit
e95613f3fe
100 changed files with 466 additions and 666 deletions
|
|
@ -277,7 +277,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSetUpRecoveryClick() {
|
override fun onSetUpRecoveryClick() {
|
||||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.SetUpRecovery))
|
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.Root))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSessionConfirmRecoveryKeyClick() {
|
override fun onSessionConfirmRecoveryKeyClick() {
|
||||||
|
|
|
||||||
|
|
@ -138,8 +138,8 @@ private fun ColumnScope.ManageAppSection(
|
||||||
}
|
}
|
||||||
if (state.showSecureBackup) {
|
if (state.showSecureBackup) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(id = CommonStrings.common_chat_backup)) },
|
headlineContent = { Text(stringResource(id = CommonStrings.common_encryption)) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.KeySolid())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Key())),
|
||||||
trailingContent = ListItemContent.Badge.takeIf { state.showSecureBackupBadge },
|
trailingContent = ListItemContent.Badge.takeIf { state.showSecureBackupBadge },
|
||||||
onClick = onSecureBackupClick,
|
onClick = onSecureBackupClick,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ internal fun SetUpRecoveryKeyBanner(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
title = stringResource(R.string.banner_set_up_recovery_title),
|
title = stringResource(R.string.banner_set_up_recovery_title),
|
||||||
content = stringResource(R.string.banner_set_up_recovery_content),
|
content = stringResource(R.string.banner_set_up_recovery_content),
|
||||||
|
actionText = stringResource(R.string.banner_set_up_recovery_submit),
|
||||||
onSubmitClick = onContinueClick,
|
onSubmitClick = onContinueClick,
|
||||||
onDismissClick = onDismissClick,
|
onDismissClick = onDismissClick,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@
|
||||||
<string name="banner_migrate_to_native_sliding_sync_description">"Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."</string>
|
<string name="banner_migrate_to_native_sliding_sync_description">"Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."</string>
|
||||||
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."</string>
|
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."</string>
|
||||||
<string name="banner_migrate_to_native_sliding_sync_title">"Upgrade available"</string>
|
<string name="banner_migrate_to_native_sliding_sync_title">"Upgrade available"</string>
|
||||||
<string name="banner_set_up_recovery_content">"Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."</string>
|
<string name="banner_set_up_recovery_content">"Recover your cryptographic identity and message history with a recovery key if you have lost all your existing devices."</string>
|
||||||
<string name="banner_set_up_recovery_title">"Set up recovery"</string>
|
<string name="banner_set_up_recovery_submit">"Set up recovery"</string>
|
||||||
|
<string name="banner_set_up_recovery_title">"Set up recovery to protect your account"</string>
|
||||||
<string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."</string>
|
<string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."</string>
|
||||||
<string name="confirm_recovery_key_banner_title">"Enter your recovery key"</string>
|
<string name="confirm_recovery_key_banner_title">"Enter your recovery key"</string>
|
||||||
<string name="full_screen_intent_banner_message">"To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked."</string>
|
<string name="full_screen_intent_banner_message">"To ensure you never miss an important call, please change your settings to allow full-screen notifications when your phone is locked."</string>
|
||||||
|
|
|
||||||
|
|
@ -121,12 +121,9 @@ class RoomListViewTest {
|
||||||
),
|
),
|
||||||
onSetUpRecoveryClick = callback,
|
onSetUpRecoveryClick = callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove automatic initial events
|
// Remove automatic initial events
|
||||||
eventsRecorder.clear()
|
eventsRecorder.clear()
|
||||||
|
rule.clickOn(R.string.banner_set_up_recovery_submit)
|
||||||
rule.clickOn(CommonStrings.action_continue)
|
|
||||||
|
|
||||||
eventsRecorder.assertEmpty()
|
eventsRecorder.assertEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||||
import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode
|
import io.element.android.features.securebackup.impl.disable.SecureBackupDisableNode
|
||||||
import io.element.android.features.securebackup.impl.enable.SecureBackupEnableNode
|
|
||||||
import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode
|
import io.element.android.features.securebackup.impl.enter.SecureBackupEnterRecoveryKeyNode
|
||||||
import io.element.android.features.securebackup.impl.reset.ResetIdentityFlowNode
|
import io.element.android.features.securebackup.impl.reset.ResetIdentityFlowNode
|
||||||
import io.element.android.features.securebackup.impl.root.SecureBackupRootNode
|
import io.element.android.features.securebackup.impl.root.SecureBackupRootNode
|
||||||
|
|
@ -63,9 +62,6 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Disable : NavTarget
|
data object Disable : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data object Enable : NavTarget
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object EnterRecoveryKey : NavTarget
|
data object EnterRecoveryKey : NavTarget
|
||||||
|
|
||||||
|
|
@ -91,10 +87,6 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.Disable)
|
backstack.push(NavTarget.Disable)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEnableClick() {
|
|
||||||
backstack.push(NavTarget.Enable)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfirmRecoveryKeyClick() {
|
override fun onConfirmRecoveryKeyClick() {
|
||||||
backstack.push(NavTarget.EnterRecoveryKey)
|
backstack.push(NavTarget.EnterRecoveryKey)
|
||||||
}
|
}
|
||||||
|
|
@ -116,9 +108,6 @@ class SecureBackupFlowNode @AssistedInject constructor(
|
||||||
NavTarget.Disable -> {
|
NavTarget.Disable -> {
|
||||||
createNode<SecureBackupDisableNode>(buildContext)
|
createNode<SecureBackupDisableNode>(buildContext)
|
||||||
}
|
}
|
||||||
NavTarget.Enable -> {
|
|
||||||
createNode<SecureBackupEnableNode>(buildContext)
|
|
||||||
}
|
|
||||||
NavTarget.EnterRecoveryKey -> {
|
NavTarget.EnterRecoveryKey -> {
|
||||||
val callback = object : SecureBackupEnterRecoveryKeyNode.Callback {
|
val callback = object : SecureBackupEnterRecoveryKeyNode.Callback {
|
||||||
override fun onEnterRecoveryKeySuccess() {
|
override fun onEnterRecoveryKeySuccess() {
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,7 @@ class SecureBackupDisablePresenter @Inject constructor(
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
fun handleEvents(event: SecureBackupDisableEvents) {
|
fun handleEvents(event: SecureBackupDisableEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is SecureBackupDisableEvents.DisableBackup -> if (disableAction.value.isConfirming()) {
|
is SecureBackupDisableEvents.DisableBackup -> coroutineScope.disableBackup(disableAction)
|
||||||
coroutineScope.disableBackup(disableAction)
|
|
||||||
} else {
|
|
||||||
disableAction.value = AsyncAction.ConfirmingNoParams
|
|
||||||
}
|
|
||||||
SecureBackupDisableEvents.DismissDialogs -> {
|
SecureBackupDisableEvents.DismissDialogs -> {
|
||||||
disableAction.value = AsyncAction.Uninitialized
|
disableAction.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.disable
|
package io.element.android.features.securebackup.impl.disable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
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.ColumnScope
|
||||||
|
|
@ -25,7 +26,6 @@ import io.element.android.features.securebackup.impl.R
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
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.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
|
||||||
|
|
@ -44,7 +44,7 @@ fun SecureBackupDisableView(
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
title = stringResource(id = R.string.screen_key_backup_disable_title),
|
title = stringResource(id = R.string.screen_key_backup_disable_title),
|
||||||
subTitle = stringResource(id = R.string.screen_key_backup_disable_description),
|
subTitle = stringResource(id = R.string.screen_key_backup_disable_description),
|
||||||
iconStyle = BigIcon.Style.Default(CompoundIcons.KeyOffSolid()),
|
iconStyle = BigIcon.Style.AlertSolid,
|
||||||
buttons = { Buttons(state = state) },
|
buttons = { Buttons(state = state) },
|
||||||
) {
|
) {
|
||||||
Content(state = state)
|
Content(state = state)
|
||||||
|
|
@ -52,12 +52,6 @@ fun SecureBackupDisableView(
|
||||||
|
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
async = state.disableAction,
|
async = state.disableAction,
|
||||||
confirmationDialog = {
|
|
||||||
SecureBackupDisableConfirmationDialog(
|
|
||||||
onConfirm = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup) },
|
|
||||||
onDismiss = { state.eventSink.invoke(SecureBackupDisableEvents.DismissDialogs) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
progressDialog = {},
|
progressDialog = {},
|
||||||
errorMessage = { it.message ?: it.toString() },
|
errorMessage = { it.message ?: it.toString() },
|
||||||
onErrorDismiss = { state.eventSink.invoke(SecureBackupDisableEvents.DismissDialogs) },
|
onErrorDismiss = { state.eventSink.invoke(SecureBackupDisableEvents.DismissDialogs) },
|
||||||
|
|
@ -65,18 +59,6 @@ fun SecureBackupDisableView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SecureBackupDisableConfirmationDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
|
|
||||||
ConfirmationDialog(
|
|
||||||
title = stringResource(id = R.string.screen_key_backup_disable_confirmation_title),
|
|
||||||
content = stringResource(id = R.string.screen_key_backup_disable_confirmation_description),
|
|
||||||
submitText = stringResource(id = R.string.screen_key_backup_disable_confirmation_action_turn_off),
|
|
||||||
destructiveSubmit = true,
|
|
||||||
onSubmitClick = onConfirm,
|
|
||||||
onDismiss = onDismiss,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.Buttons(
|
private fun ColumnScope.Buttons(
|
||||||
state: SecureBackupDisableState,
|
state: SecureBackupDisableState,
|
||||||
|
|
@ -105,15 +87,20 @@ private fun Content(state: SecureBackupDisableState) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SecureBackupDisableItem(text: String) {
|
private fun SecureBackupDisableItem(text: String) {
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(color = ElementTheme.colors.bgActionSecondaryHovered)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = CompoundIcons.Close(),
|
imageVector = CompoundIcons.Close(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = ElementTheme.colors.iconCriticalPrimary,
|
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 8.dp, end = 4.dp),
|
|
||||||
text = text,
|
text = text,
|
||||||
color = ElementTheme.colors.textSecondary,
|
color = ElementTheme.colors.textSecondary,
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.enable
|
|
||||||
|
|
||||||
sealed interface SecureBackupEnableEvents {
|
|
||||||
data object EnableBackup : SecureBackupEnableEvents
|
|
||||||
data object DismissDialog : SecureBackupEnableEvents
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.enable
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
|
||||||
import com.bumble.appyx.core.node.Node
|
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
|
||||||
class SecureBackupEnableNode @AssistedInject constructor(
|
|
||||||
@Assisted buildContext: BuildContext,
|
|
||||||
@Assisted plugins: List<Plugin>,
|
|
||||||
private val presenter: SecureBackupEnablePresenter,
|
|
||||||
) : Node(buildContext, plugins = plugins) {
|
|
||||||
@Composable
|
|
||||||
override fun View(modifier: Modifier) {
|
|
||||||
val state = presenter.present()
|
|
||||||
SecureBackupEnableView(
|
|
||||||
state = state,
|
|
||||||
modifier = modifier,
|
|
||||||
onSuccess = ::navigateUp,
|
|
||||||
onBackClick = ::navigateUp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.enable
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import io.element.android.features.securebackup.impl.loggerTagDisable
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class SecureBackupEnablePresenter @Inject constructor(
|
|
||||||
private val encryptionService: EncryptionService,
|
|
||||||
) : Presenter<SecureBackupEnableState> {
|
|
||||||
@Composable
|
|
||||||
override fun present(): SecureBackupEnableState {
|
|
||||||
val enableAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
fun handleEvents(event: SecureBackupEnableEvents) {
|
|
||||||
when (event) {
|
|
||||||
is SecureBackupEnableEvents.EnableBackup ->
|
|
||||||
coroutineScope.enableBackup(enableAction)
|
|
||||||
SecureBackupEnableEvents.DismissDialog -> {
|
|
||||||
enableAction.value = AsyncAction.Uninitialized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return SecureBackupEnableState(
|
|
||||||
enableAction = enableAction.value,
|
|
||||||
eventSink = ::handleEvents
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CoroutineScope.enableBackup(action: MutableState<AsyncAction<Unit>>) = launch {
|
|
||||||
suspend {
|
|
||||||
Timber.tag(loggerTagDisable.value).d("Calling encryptionService.enableBackups()")
|
|
||||||
encryptionService.enableBackups().getOrThrow()
|
|
||||||
}.runCatchingUpdatingState(action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.enable
|
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
|
|
||||||
data class SecureBackupEnableState(
|
|
||||||
val enableAction: AsyncAction<Unit>,
|
|
||||||
val eventSink: (SecureBackupEnableEvents) -> Unit
|
|
||||||
)
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.enable
|
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
|
|
||||||
open class SecureBackupEnableStateProvider : PreviewParameterProvider<SecureBackupEnableState> {
|
|
||||||
override val values: Sequence<SecureBackupEnableState>
|
|
||||||
get() = sequenceOf(
|
|
||||||
aSecureBackupEnableState(),
|
|
||||||
aSecureBackupEnableState(enableAction = AsyncAction.Loading),
|
|
||||||
aSecureBackupEnableState(enableAction = AsyncAction.Failure(Exception("Failed to enable"))),
|
|
||||||
// Add other states here
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun aSecureBackupEnableState(
|
|
||||||
enableAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
|
||||||
) = SecureBackupEnableState(
|
|
||||||
enableAction = enableAction,
|
|
||||||
eventSink = {}
|
|
||||||
)
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.enable
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
|
||||||
import io.element.android.features.securebackup.impl.R
|
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
|
||||||
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
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SecureBackupEnableView(
|
|
||||||
state: SecureBackupEnableState,
|
|
||||||
onSuccess: () -> Unit,
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
FlowStepPage(
|
|
||||||
modifier = modifier,
|
|
||||||
onBackClick = onBackClick,
|
|
||||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
|
|
||||||
iconStyle = BigIcon.Style.Default(CompoundIcons.KeySolid()),
|
|
||||||
buttons = { Buttons(state = state) }
|
|
||||||
)
|
|
||||||
AsyncActionView(
|
|
||||||
async = state.enableAction,
|
|
||||||
progressDialog = { },
|
|
||||||
onSuccess = { onSuccess() },
|
|
||||||
onErrorDismiss = { state.eventSink.invoke(SecureBackupEnableEvents.DismissDialog) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ColumnScope.Buttons(
|
|
||||||
state: SecureBackupEnableState,
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
text = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
|
|
||||||
showProgress = state.enableAction.isLoading(),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
onClick = { state.eventSink.invoke(SecureBackupEnableEvents.EnableBackup) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun SecureBackupEnableViewPreview(
|
|
||||||
@PreviewParameter(SecureBackupEnableStateProvider::class) state: SecureBackupEnableState
|
|
||||||
) = ElementPreview {
|
|
||||||
SecureBackupEnableView(
|
|
||||||
state = state,
|
|
||||||
onSuccess = {},
|
|
||||||
onBackClick = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -9,4 +9,7 @@ package io.element.android.features.securebackup.impl.root
|
||||||
|
|
||||||
sealed interface SecureBackupRootEvents {
|
sealed interface SecureBackupRootEvents {
|
||||||
data object RetryKeyBackupState : SecureBackupRootEvents
|
data object RetryKeyBackupState : SecureBackupRootEvents
|
||||||
|
data object EnableKeyStorage : SecureBackupRootEvents
|
||||||
|
data object DisplayKeyStorageDisabledError : SecureBackupRootEvents
|
||||||
|
data object DismissDialog : SecureBackupRootEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ class SecureBackupRootNode @AssistedInject constructor(
|
||||||
fun onSetupClick()
|
fun onSetupClick()
|
||||||
fun onChangeClick()
|
fun onChangeClick()
|
||||||
fun onDisableClick()
|
fun onDisableClick()
|
||||||
fun onEnableClick()
|
|
||||||
fun onConfirmRecoveryKeyClick()
|
fun onConfirmRecoveryKeyClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,10 +49,6 @@ class SecureBackupRootNode @AssistedInject constructor(
|
||||||
plugins<Callback>().forEach { it.onDisableClick() }
|
plugins<Callback>().forEach { it.onDisableClick() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onEnableClick() {
|
|
||||||
plugins<Callback>().forEach { it.onEnableClick() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onConfirmRecoveryKeyClick() {
|
private fun onConfirmRecoveryKeyClick() {
|
||||||
plugins<Callback>().forEach { it.onConfirmRecoveryKeyClick() }
|
plugins<Callback>().forEach { it.onConfirmRecoveryKeyClick() }
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +66,6 @@ class SecureBackupRootNode @AssistedInject constructor(
|
||||||
onBackClick = ::navigateUp,
|
onBackClick = ::navigateUp,
|
||||||
onSetupClick = ::onSetupClick,
|
onSetupClick = ::onSetupClick,
|
||||||
onChangeClick = ::onChangeClick,
|
onChangeClick = ::onChangeClick,
|
||||||
onEnableClick = ::onEnableClick,
|
|
||||||
onDisableClick = ::onDisableClick,
|
onDisableClick = ::onDisableClick,
|
||||||
onConfirmRecoveryKeyClick = ::onConfirmRecoveryKeyClick,
|
onConfirmRecoveryKeyClick = ::onConfirmRecoveryKeyClick,
|
||||||
onLearnMoreClick = { onLearnMoreClick(uriHandler) },
|
onLearnMoreClick = { onLearnMoreClick(uriHandler) },
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,10 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.securebackup.impl.loggerTagDisable
|
||||||
import io.element.android.features.securebackup.impl.loggerTagRoot
|
import io.element.android.features.securebackup.impl.loggerTagRoot
|
||||||
|
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.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
|
@ -41,7 +44,8 @@ class SecureBackupRootPresenter @Inject constructor(
|
||||||
|
|
||||||
val backupState by encryptionService.backupStateStateFlow.collectAsState()
|
val backupState by encryptionService.backupStateStateFlow.collectAsState()
|
||||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||||
|
val enableAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
var displayKeyStorageDisabledError by remember { mutableStateOf(false) }
|
||||||
Timber.tag(loggerTagRoot.value).d("backupState: $backupState")
|
Timber.tag(loggerTagRoot.value).d("backupState: $backupState")
|
||||||
Timber.tag(loggerTagRoot.value).d("recoveryState: $recoveryState")
|
Timber.tag(loggerTagRoot.value).d("recoveryState: $recoveryState")
|
||||||
|
|
||||||
|
|
@ -56,14 +60,22 @@ class SecureBackupRootPresenter @Inject constructor(
|
||||||
fun handleEvents(event: SecureBackupRootEvents) {
|
fun handleEvents(event: SecureBackupRootEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
SecureBackupRootEvents.RetryKeyBackupState -> localCoroutineScope.getKeyBackupStatus(doesBackupExistOnServerAction)
|
SecureBackupRootEvents.RetryKeyBackupState -> localCoroutineScope.getKeyBackupStatus(doesBackupExistOnServerAction)
|
||||||
|
SecureBackupRootEvents.EnableKeyStorage -> localCoroutineScope.enableBackup(enableAction)
|
||||||
|
SecureBackupRootEvents.DismissDialog -> {
|
||||||
|
enableAction.value = AsyncAction.Uninitialized
|
||||||
|
displayKeyStorageDisabledError = false
|
||||||
|
}
|
||||||
|
SecureBackupRootEvents.DisplayKeyStorageDisabledError -> displayKeyStorageDisabledError = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SecureBackupRootState(
|
return SecureBackupRootState(
|
||||||
|
enableAction = enableAction.value,
|
||||||
backupState = backupState,
|
backupState = backupState,
|
||||||
doesBackupExistOnServer = doesBackupExistOnServerAction.value,
|
doesBackupExistOnServer = doesBackupExistOnServerAction.value,
|
||||||
recoveryState = recoveryState,
|
recoveryState = recoveryState,
|
||||||
appName = buildMeta.applicationName,
|
appName = buildMeta.applicationName,
|
||||||
|
displayKeyStorageDisabledError = displayKeyStorageDisabledError,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
|
|
@ -74,4 +86,11 @@ class SecureBackupRootPresenter @Inject constructor(
|
||||||
encryptionService.doesBackupExistOnServer().getOrThrow()
|
encryptionService.doesBackupExistOnServer().getOrThrow()
|
||||||
}.runCatchingUpdatingState(action)
|
}.runCatchingUpdatingState(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.enableBackup(action: MutableState<AsyncAction<Unit>>) = launch {
|
||||||
|
suspend {
|
||||||
|
Timber.tag(loggerTagDisable.value).d("Calling encryptionService.enableBackups()")
|
||||||
|
encryptionService.enableBackups().getOrThrow()
|
||||||
|
}.runCatchingUpdatingState(action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,31 @@
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.root
|
package io.element.android.features.securebackup.impl.root
|
||||||
|
|
||||||
|
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.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
|
|
||||||
data class SecureBackupRootState(
|
data class SecureBackupRootState(
|
||||||
|
val enableAction: AsyncAction<Unit>,
|
||||||
val backupState: BackupState,
|
val backupState: BackupState,
|
||||||
val doesBackupExistOnServer: AsyncData<Boolean>,
|
val doesBackupExistOnServer: AsyncData<Boolean>,
|
||||||
val recoveryState: RecoveryState,
|
val recoveryState: RecoveryState,
|
||||||
val appName: String,
|
val appName: String,
|
||||||
|
val displayKeyStorageDisabledError: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val eventSink: (SecureBackupRootEvents) -> Unit,
|
val eventSink: (SecureBackupRootEvents) -> Unit,
|
||||||
)
|
) {
|
||||||
|
val isKeyStorageEnabled: Boolean
|
||||||
|
get() = when (backupState) {
|
||||||
|
BackupState.UNKNOWN -> doesBackupExistOnServer.dataOrNull() == true
|
||||||
|
BackupState.CREATING,
|
||||||
|
BackupState.ENABLING,
|
||||||
|
BackupState.RESUMING,
|
||||||
|
BackupState.DOWNLOADING,
|
||||||
|
BackupState.ENABLED -> true
|
||||||
|
BackupState.WAITING_FOR_SYNC,
|
||||||
|
BackupState.DISABLING -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.securebackup.impl.root
|
package io.element.android.features.securebackup.impl.root
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
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.designsystem.utils.snackbar.SnackbarMessage
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||||
|
|
@ -22,28 +23,47 @@ open class SecureBackupRootStateProvider : PreviewParameterProvider<SecureBackup
|
||||||
aSecureBackupRootState(backupState = BackupState.UNKNOWN, doesBackupExistOnServer = AsyncData.Failure(Exception("An error"))),
|
aSecureBackupRootState(backupState = BackupState.UNKNOWN, doesBackupExistOnServer = AsyncData.Failure(Exception("An error"))),
|
||||||
aSecureBackupRootState(backupState = BackupState.WAITING_FOR_SYNC),
|
aSecureBackupRootState(backupState = BackupState.WAITING_FOR_SYNC),
|
||||||
aSecureBackupRootState(backupState = BackupState.CREATING),
|
aSecureBackupRootState(backupState = BackupState.CREATING),
|
||||||
|
aSecureBackupRootState(
|
||||||
|
backupState = BackupState.CREATING,
|
||||||
|
enableAction = AsyncAction.Failure(Exception("Error")),
|
||||||
|
),
|
||||||
|
aSecureBackupRootState(backupState = BackupState.ENABLING),
|
||||||
aSecureBackupRootState(backupState = BackupState.RESUMING),
|
aSecureBackupRootState(backupState = BackupState.RESUMING),
|
||||||
aSecureBackupRootState(backupState = BackupState.DOWNLOADING),
|
aSecureBackupRootState(backupState = BackupState.DOWNLOADING),
|
||||||
aSecureBackupRootState(backupState = BackupState.DISABLING),
|
aSecureBackupRootState(backupState = BackupState.DISABLING),
|
||||||
aSecureBackupRootState(backupState = BackupState.ENABLED),
|
aSecureBackupRootState(backupState = BackupState.ENABLED),
|
||||||
aSecureBackupRootState(recoveryState = RecoveryState.UNKNOWN),
|
aSecureBackupRootState(backupState = BackupState.ENABLED, recoveryState = RecoveryState.UNKNOWN),
|
||||||
aSecureBackupRootState(recoveryState = RecoveryState.ENABLED),
|
aSecureBackupRootState(backupState = BackupState.ENABLED, recoveryState = RecoveryState.ENABLED),
|
||||||
aSecureBackupRootState(recoveryState = RecoveryState.DISABLED),
|
aSecureBackupRootState(backupState = BackupState.ENABLED, recoveryState = RecoveryState.DISABLED),
|
||||||
aSecureBackupRootState(recoveryState = RecoveryState.INCOMPLETE),
|
aSecureBackupRootState(backupState = BackupState.ENABLED, recoveryState = RecoveryState.INCOMPLETE),
|
||||||
// Add other states here
|
aSecureBackupRootState(
|
||||||
|
backupState = BackupState.UNKNOWN,
|
||||||
|
doesBackupExistOnServer = AsyncData.Success(false),
|
||||||
|
recoveryState = RecoveryState.ENABLED,
|
||||||
|
),
|
||||||
|
aSecureBackupRootState(
|
||||||
|
backupState = BackupState.UNKNOWN,
|
||||||
|
doesBackupExistOnServer = AsyncData.Success(false),
|
||||||
|
recoveryState = RecoveryState.ENABLED,
|
||||||
|
displayKeyStorageDisabledError = true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aSecureBackupRootState(
|
fun aSecureBackupRootState(
|
||||||
|
enableAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
backupState: BackupState = BackupState.UNKNOWN,
|
backupState: BackupState = BackupState.UNKNOWN,
|
||||||
doesBackupExistOnServer: AsyncData<Boolean> = AsyncData.Uninitialized,
|
doesBackupExistOnServer: AsyncData<Boolean> = AsyncData.Uninitialized,
|
||||||
recoveryState: RecoveryState = RecoveryState.UNKNOWN,
|
recoveryState: RecoveryState = RecoveryState.UNKNOWN,
|
||||||
|
displayKeyStorageDisabledError: Boolean = false,
|
||||||
snackbarMessage: SnackbarMessage? = null,
|
snackbarMessage: SnackbarMessage? = null,
|
||||||
) = SecureBackupRootState(
|
) = SecureBackupRootState(
|
||||||
|
enableAction = enableAction,
|
||||||
backupState = backupState,
|
backupState = backupState,
|
||||||
doesBackupExistOnServer = doesBackupExistOnServer,
|
doesBackupExistOnServer = doesBackupExistOnServer,
|
||||||
recoveryState = recoveryState,
|
recoveryState = recoveryState,
|
||||||
appName = "Element",
|
appName = "Element",
|
||||||
|
displayKeyStorageDisabledError = displayKeyStorageDisabledError,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,27 @@
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.root
|
package io.element.android.features.securebackup.impl.root
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.progressSemantics
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
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
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.securebackup.impl.R
|
import io.element.android.features.securebackup.impl.R
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncLoading
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceDivider
|
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
import io.element.android.libraries.designsystem.components.preferences.PreferencePage
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
|
||||||
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.text.buildAnnotatedStringWithStyledPart
|
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.ListItem
|
import io.element.android.libraries.designsystem.theme.components.ListItem
|
||||||
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.TextButton
|
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||||
|
|
@ -41,7 +40,6 @@ fun SecureBackupRootView(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onSetupClick: () -> Unit,
|
onSetupClick: () -> Unit,
|
||||||
onChangeClick: () -> Unit,
|
onChangeClick: () -> Unit,
|
||||||
onEnableClick: () -> Unit,
|
|
||||||
onDisableClick: () -> Unit,
|
onDisableClick: () -> Unit,
|
||||||
onConfirmRecoveryKeyClick: () -> Unit,
|
onConfirmRecoveryKeyClick: () -> Unit,
|
||||||
onLearnMoreClick: () -> Unit,
|
onLearnMoreClick: () -> Unit,
|
||||||
|
|
@ -52,122 +50,186 @@ fun SecureBackupRootView(
|
||||||
PreferencePage(
|
PreferencePage(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
title = stringResource(id = CommonStrings.common_chat_backup),
|
title = stringResource(id = CommonStrings.common_encryption),
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
) {
|
) {
|
||||||
val text = buildAnnotatedStringWithStyledPart(
|
ListItem(
|
||||||
fullTextRes = R.string.screen_chat_backup_key_backup_description,
|
headlineContent = {
|
||||||
coloredTextRes = CommonStrings.action_learn_more,
|
Text(
|
||||||
color = ElementTheme.colors.textPrimary,
|
text = stringResource(id = R.string.screen_chat_backup_key_backup_title),
|
||||||
underline = false,
|
)
|
||||||
bold = true,
|
},
|
||||||
)
|
supportingContent = {
|
||||||
PreferenceText(
|
Text(
|
||||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_title),
|
text = buildAnnotatedStringWithStyledPart(
|
||||||
subtitleAnnotated = text,
|
fullTextRes = R.string.screen_chat_backup_key_backup_description,
|
||||||
|
coloredTextRes = CommonStrings.action_learn_more,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
underline = false,
|
||||||
|
bold = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
onClick = onLearnMoreClick,
|
onClick = onLearnMoreClick,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Disable / Enable backup
|
// Disable / Enable key storage
|
||||||
when (state.backupState) {
|
ListItem(
|
||||||
BackupState.WAITING_FOR_SYNC -> Unit
|
headlineContent = {
|
||||||
BackupState.UNKNOWN -> {
|
Text(
|
||||||
when (state.doesBackupExistOnServer) {
|
text = stringResource(id = R.string.screen_chat_backup_key_storage_toggle_title),
|
||||||
is AsyncData.Success -> when (state.doesBackupExistOnServer.data) {
|
)
|
||||||
true -> {
|
},
|
||||||
PreferenceText(
|
trailingContent = when (state.backupState) {
|
||||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable),
|
BackupState.WAITING_FOR_SYNC,
|
||||||
tintColor = ElementTheme.colors.textCriticalPrimary,
|
BackupState.DISABLING -> ListItemContent.Custom { LoadingView() }
|
||||||
onClick = onDisableClick,
|
BackupState.UNKNOWN -> {
|
||||||
|
when (state.doesBackupExistOnServer) {
|
||||||
|
is AsyncData.Success -> {
|
||||||
|
ListItemContent.Switch(checked = state.doesBackupExistOnServer.data)
|
||||||
|
}
|
||||||
|
is AsyncData.Loading,
|
||||||
|
AsyncData.Uninitialized -> ListItemContent.Custom { LoadingView() }
|
||||||
|
is AsyncData.Failure -> ListItemContent.Custom {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = CommonStrings.action_retry)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
false -> {
|
|
||||||
PreferenceText(
|
|
||||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
|
|
||||||
onClick = onEnableClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is AsyncData.Loading,
|
|
||||||
AsyncData.Uninitialized -> {
|
|
||||||
ListItem(headlineContent = {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
is AsyncData.Failure -> {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = CommonStrings.error_unknown),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailingContent = ListItemContent.Custom {
|
|
||||||
TextButton(
|
|
||||||
text = stringResource(
|
|
||||||
id = CommonStrings.action_retry
|
|
||||||
),
|
|
||||||
onClick = { state.eventSink.invoke(SecureBackupRootEvents.RetryKeyBackupState) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
PreferenceText(
|
|
||||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_enable),
|
|
||||||
onClick = onEnableClick,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
BackupState.CREATING,
|
||||||
BackupState.CREATING,
|
BackupState.ENABLING,
|
||||||
BackupState.ENABLING,
|
BackupState.RESUMING,
|
||||||
BackupState.RESUMING,
|
BackupState.ENABLED,
|
||||||
BackupState.ENABLED,
|
BackupState.DOWNLOADING -> ListItemContent.Switch(checked = true)
|
||||||
BackupState.DOWNLOADING -> {
|
},
|
||||||
PreferenceText(
|
onClick = {
|
||||||
title = stringResource(id = R.string.screen_chat_backup_key_backup_action_disable),
|
when (state.backupState) {
|
||||||
tintColor = ElementTheme.colors.textCriticalPrimary,
|
BackupState.WAITING_FOR_SYNC,
|
||||||
onClick = onDisableClick,
|
BackupState.DISABLING -> Unit
|
||||||
)
|
BackupState.UNKNOWN -> {
|
||||||
}
|
when (state.doesBackupExistOnServer) {
|
||||||
BackupState.DISABLING -> {
|
is AsyncData.Success -> {
|
||||||
AsyncLoading()
|
if (state.doesBackupExistOnServer.data) {
|
||||||
}
|
onDisableClick()
|
||||||
}
|
} else {
|
||||||
|
state.eventSink.invoke(SecureBackupRootEvents.EnableKeyStorage)
|
||||||
PreferenceDivider()
|
}
|
||||||
|
}
|
||||||
|
is AsyncData.Loading,
|
||||||
|
AsyncData.Uninitialized -> Unit
|
||||||
|
is AsyncData.Failure -> state.eventSink.invoke(SecureBackupRootEvents.RetryKeyBackupState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackupState.CREATING,
|
||||||
|
BackupState.ENABLING,
|
||||||
|
BackupState.RESUMING,
|
||||||
|
BackupState.ENABLED,
|
||||||
|
BackupState.DOWNLOADING -> onDisableClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
// Setup recovery
|
// Setup recovery
|
||||||
when (state.recoveryState) {
|
when (state.recoveryState) {
|
||||||
RecoveryState.UNKNOWN,
|
RecoveryState.UNKNOWN,
|
||||||
RecoveryState.WAITING_FOR_SYNC -> Unit
|
RecoveryState.WAITING_FOR_SYNC -> Unit
|
||||||
RecoveryState.DISABLED -> {
|
RecoveryState.DISABLED -> {
|
||||||
PreferenceText(
|
ListItem(
|
||||||
title = stringResource(id = R.string.screen_chat_backup_recovery_action_setup),
|
headlineContent = {
|
||||||
subtitle = stringResource(id = R.string.screen_chat_backup_recovery_action_setup_description, state.appName),
|
Text(
|
||||||
onClick = onSetupClick,
|
text = stringResource(id = R.string.screen_chat_backup_recovery_action_setup),
|
||||||
showEndBadge = true,
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.screen_chat_backup_recovery_action_setup_description, state.appName),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = ListItemContent.Badge,
|
||||||
|
enabled = state.isKeyStorageEnabled,
|
||||||
|
alwaysClickable = true,
|
||||||
|
onClick = {
|
||||||
|
if (state.isKeyStorageEnabled) {
|
||||||
|
onSetupClick()
|
||||||
|
} else {
|
||||||
|
state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
RecoveryState.ENABLED -> {
|
RecoveryState.ENABLED -> {
|
||||||
PreferenceText(
|
ListItem(
|
||||||
title = stringResource(id = R.string.screen_chat_backup_recovery_action_change),
|
headlineContent = {
|
||||||
onClick = onChangeClick,
|
Text(
|
||||||
|
text = stringResource(id = R.string.screen_chat_backup_recovery_action_change),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.screen_chat_backup_recovery_action_change_description),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
enabled = state.isKeyStorageEnabled,
|
||||||
|
alwaysClickable = true,
|
||||||
|
onClick = {
|
||||||
|
if (state.isKeyStorageEnabled) {
|
||||||
|
onChangeClick()
|
||||||
|
} else {
|
||||||
|
state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
RecoveryState.INCOMPLETE ->
|
RecoveryState.INCOMPLETE ->
|
||||||
PreferenceText(
|
ListItem(
|
||||||
title = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm),
|
headlineContent = {
|
||||||
subtitle = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm_description),
|
Text(
|
||||||
showEndBadge = true,
|
text = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm),
|
||||||
onClick = onConfirmRecoveryKeyClick,
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.screen_chat_backup_recovery_action_confirm_description),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = ListItemContent.Badge,
|
||||||
|
enabled = state.isKeyStorageEnabled,
|
||||||
|
alwaysClickable = true,
|
||||||
|
onClick = {
|
||||||
|
if (state.isKeyStorageEnabled) {
|
||||||
|
onConfirmRecoveryKeyClick()
|
||||||
|
} else {
|
||||||
|
state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AsyncActionView(
|
||||||
|
async = state.enableAction,
|
||||||
|
progressDialog = { },
|
||||||
|
onSuccess = { },
|
||||||
|
onErrorDismiss = { state.eventSink.invoke(SecureBackupRootEvents.DismissDialog) }
|
||||||
|
)
|
||||||
|
if (state.displayKeyStorageDisabledError) {
|
||||||
|
ErrorDialog(
|
||||||
|
title = null,
|
||||||
|
content = stringResource(id = R.string.screen_chat_backup_key_storage_disabled_error),
|
||||||
|
onSubmit = { state.eventSink.invoke(SecureBackupRootEvents.DismissDialog) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LoadingView() {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.progressSemantics()
|
||||||
|
.size(24.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
|
|
@ -180,7 +242,6 @@ internal fun SecureBackupRootViewPreview(
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
onSetupClick = {},
|
onSetupClick = {},
|
||||||
onChangeClick = {},
|
onChangeClick = {},
|
||||||
onEnableClick = {},
|
|
||||||
onDisableClick = {},
|
onDisableClick = {},
|
||||||
onConfirmRecoveryKeyClick = {},
|
onConfirmRecoveryKeyClick = {},
|
||||||
onLearnMoreClick = {},
|
onLearnMoreClick = {},
|
||||||
|
|
|
||||||
|
|
@ -91,14 +91,14 @@ private fun RecoveryKeyStaticContent(
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(14.dp))
|
.clip(RoundedCornerShape(14.dp))
|
||||||
.background(
|
.background(
|
||||||
color = ElementTheme.colors.bgSubtleSecondary,
|
color = ElementTheme.colors.bgSubtleSecondary,
|
||||||
shape = RoundedCornerShape(14.dp)
|
shape = RoundedCornerShape(14.dp)
|
||||||
)
|
)
|
||||||
.clickableIfNotNull(onClick)
|
.clickableIfNotNull(onClick)
|
||||||
.padding(horizontal = 16.dp, vertical = 16.dp),
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
if (state.formattedRecoveryKey != null) {
|
if (state.formattedRecoveryKey != null) {
|
||||||
|
|
@ -116,15 +116,15 @@ private fun RecoveryKeyStaticContent(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 11.dp)
|
.padding(vertical = 11.dp)
|
||||||
) {
|
) {
|
||||||
if (state.inProgress) {
|
if (state.inProgress) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.progressSemantics()
|
.progressSemantics()
|
||||||
.padding(end = 8.dp)
|
.padding(end = 8.dp)
|
||||||
.size(16.dp),
|
.size(16.dp),
|
||||||
color = ElementTheme.colors.textPrimary,
|
color = ElementTheme.colors.textPrimary,
|
||||||
strokeWidth = 1.5.dp,
|
strokeWidth = 1.5.dp,
|
||||||
)
|
)
|
||||||
|
|
@ -161,12 +161,12 @@ private fun RecoveryKeyFormContent(
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.testTag(TestTags.recoveryKey)
|
.testTag(TestTags.recoveryKey)
|
||||||
.autofill(
|
.autofill(
|
||||||
autofillTypes = listOf(AutofillType.Password),
|
autofillTypes = listOf(AutofillType.Password),
|
||||||
onFill = { onChange(it) },
|
onFill = { onChange(it) },
|
||||||
),
|
),
|
||||||
minLines = 2,
|
minLines = 2,
|
||||||
value = state.formattedRecoveryKey.orEmpty(),
|
value = state.formattedRecoveryKey.orEmpty(),
|
||||||
onValueChange = onChange,
|
onValueChange = onChange,
|
||||||
|
|
@ -189,30 +189,18 @@ private fun RecoveryKeyFooter(state: RecoveryKeyViewState) {
|
||||||
RecoveryKeyUserStory.Setup,
|
RecoveryKeyUserStory.Setup,
|
||||||
RecoveryKeyUserStory.Change -> {
|
RecoveryKeyUserStory.Change -> {
|
||||||
if (state.formattedRecoveryKey == null) {
|
if (state.formattedRecoveryKey == null) {
|
||||||
Row(
|
Text(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
text = stringResource(
|
||||||
) {
|
id = if (state.recoveryKeyUserStory == RecoveryKeyUserStory.Change) {
|
||||||
Icon(
|
R.string.screen_recovery_key_change_generate_key_description
|
||||||
imageVector = CompoundIcons.InfoSolid(),
|
} else {
|
||||||
contentDescription = null,
|
R.string.screen_recovery_key_setup_generate_key_description
|
||||||
tint = ElementTheme.colors.iconSecondary,
|
}
|
||||||
modifier = Modifier
|
),
|
||||||
.padding(start = 16.dp)
|
color = ElementTheme.colors.textSecondary,
|
||||||
.size(20.dp),
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
)
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
Text(
|
)
|
||||||
text = stringResource(
|
|
||||||
id = if (state.recoveryKeyUserStory == RecoveryKeyUserStory.Change) {
|
|
||||||
R.string.screen_recovery_key_change_generate_key_description
|
|
||||||
} else {
|
|
||||||
R.string.screen_recovery_key_setup_generate_key_description
|
|
||||||
}
|
|
||||||
),
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.screen_recovery_key_save_key_description),
|
text = stringResource(id = R.string.screen_recovery_key_save_key_description),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_chat_backup_key_backup_action_disable">"Turn off backup"</string>
|
<string name="screen_chat_backup_key_backup_action_disable">"Delete key storage"</string>
|
||||||
<string name="screen_chat_backup_key_backup_action_enable">"Turn on backup"</string>
|
<string name="screen_chat_backup_key_backup_action_enable">"Turn on backup"</string>
|
||||||
<string name="screen_chat_backup_key_backup_description">"Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$s."</string>
|
<string name="screen_chat_backup_key_backup_description">"Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$s."</string>
|
||||||
<string name="screen_chat_backup_key_backup_title">"Key storage"</string>
|
<string name="screen_chat_backup_key_backup_title">"Key storage"</string>
|
||||||
|
<string name="screen_chat_backup_key_storage_disabled_error">"Key storage must be turned on to set up recovery."</string>
|
||||||
<string name="screen_chat_backup_key_storage_toggle_description">"Upload keys from this device"</string>
|
<string name="screen_chat_backup_key_storage_toggle_description">"Upload keys from this device"</string>
|
||||||
<string name="screen_chat_backup_key_storage_toggle_title">"Allow key storage"</string>
|
<string name="screen_chat_backup_key_storage_toggle_title">"Allow key storage"</string>
|
||||||
<string name="screen_chat_backup_recovery_action_change">"Change recovery key"</string>
|
<string name="screen_chat_backup_recovery_action_change">"Change recovery key"</string>
|
||||||
|
|
@ -28,10 +29,10 @@
|
||||||
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Turn off"</string>
|
<string name="screen_key_backup_disable_confirmation_action_turn_off">"Turn off"</string>
|
||||||
<string name="screen_key_backup_disable_confirmation_description">"You will lose your encrypted messages if you are signed out of all devices."</string>
|
<string name="screen_key_backup_disable_confirmation_description">"You will lose your encrypted messages if you are signed out of all devices."</string>
|
||||||
<string name="screen_key_backup_disable_confirmation_title">"Are you sure you want to turn off backup?"</string>
|
<string name="screen_key_backup_disable_confirmation_title">"Are you sure you want to turn off backup?"</string>
|
||||||
<string name="screen_key_backup_disable_description">"Turning off backup will remove your current encryption key backup and turn off other security features. In this case, you will:"</string>
|
<string name="screen_key_backup_disable_description">"Deleting key storage will remove your cryptographic identity and message keys from the server and turn off the following security features:"</string>
|
||||||
<string name="screen_key_backup_disable_description_point_1">"Not have encrypted message history on new devices"</string>
|
<string name="screen_key_backup_disable_description_point_1">"You will not have encrypted message history on new devices"</string>
|
||||||
<string name="screen_key_backup_disable_description_point_2">"Lose access to your encrypted messages if you are signed out of %1$s everywhere"</string>
|
<string name="screen_key_backup_disable_description_point_2">"You will lose access to your encrypted messages if you are signed out of %1$s everywhere"</string>
|
||||||
<string name="screen_key_backup_disable_title">"Are you sure you want to turn off backup?"</string>
|
<string name="screen_key_backup_disable_title">"Are you sure you want to turn off key storage and delete it?"</string>
|
||||||
<string name="screen_recovery_key_change_description">"Get a new recovery key if you\'ve lost your existing one. After changing your recovery key, your old one will no longer work."</string>
|
<string name="screen_recovery_key_change_description">"Get a new recovery key if you\'ve lost your existing one. After changing your recovery key, your old one will no longer work."</string>
|
||||||
<string name="screen_recovery_key_change_generate_key">"Generate a new recovery key"</string>
|
<string name="screen_recovery_key_change_generate_key">"Generate a new recovery key"</string>
|
||||||
<string name="screen_recovery_key_change_generate_key_description">"Do not share this with anyone!"</string>
|
<string name="screen_recovery_key_change_generate_key_description">"Do not share this with anyone!"</string>
|
||||||
|
|
@ -54,7 +55,7 @@
|
||||||
<string name="screen_recovery_key_save_title">"Save your recovery key somewhere safe"</string>
|
<string name="screen_recovery_key_save_title">"Save your recovery key somewhere safe"</string>
|
||||||
<string name="screen_recovery_key_setup_confirmation_description">"You will not be able to access your new recovery key after this step."</string>
|
<string name="screen_recovery_key_setup_confirmation_description">"You will not be able to access your new recovery key after this step."</string>
|
||||||
<string name="screen_recovery_key_setup_confirmation_title">"Have you saved your recovery key?"</string>
|
<string name="screen_recovery_key_setup_confirmation_title">"Have you saved your recovery key?"</string>
|
||||||
<string name="screen_recovery_key_setup_description">"Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting ‘Change recovery key’."</string>
|
<string name="screen_recovery_key_setup_description">"Your key storage is protected by a recovery key. If you need a new recovery key after setup, you can recreate it by selecting ‘Change recovery key’."</string>
|
||||||
<string name="screen_recovery_key_setup_generate_key">"Generate your recovery key"</string>
|
<string name="screen_recovery_key_setup_generate_key">"Generate your recovery key"</string>
|
||||||
<string name="screen_recovery_key_setup_generate_key_description">"Do not share this with anyone!"</string>
|
<string name="screen_recovery_key_setup_generate_key_description">"Do not share this with anyone!"</string>
|
||||||
<string name="screen_recovery_key_setup_success">"Recovery setup successful"</string>
|
<string name="screen_recovery_key_setup_success">"Recovery setup successful"</string>
|
||||||
|
|
|
||||||
|
|
@ -38,22 +38,6 @@ class SecureBackupDisablePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - user delete backup and cancel`() = runTest {
|
|
||||||
val presenter = createSecureBackupDisablePresenter()
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
|
||||||
val state = awaitItem()
|
|
||||||
assertThat(state.disableAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
|
||||||
initialState.eventSink(SecureBackupDisableEvents.DismissDialogs)
|
|
||||||
val finalState = awaitItem()
|
|
||||||
assertThat(finalState.disableAction).isEqualTo(AsyncAction.Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - user delete backup success`() = runTest {
|
fun `present - user delete backup success`() = runTest {
|
||||||
val presenter = createSecureBackupDisablePresenter()
|
val presenter = createSecureBackupDisablePresenter()
|
||||||
|
|
@ -63,9 +47,6 @@ class SecureBackupDisablePresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.disableAction).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(initialState.disableAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
||||||
val state = awaitItem()
|
|
||||||
assertThat(state.disableAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
|
||||||
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.disableAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(loadingState.disableAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
|
|
@ -87,9 +68,6 @@ class SecureBackupDisablePresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.disableAction).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(initialState.disableAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
||||||
val state = awaitItem()
|
|
||||||
assertThat(state.disableAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
|
||||||
initialState.eventSink(SecureBackupDisableEvents.DisableBackup)
|
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.disableAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(loadingState.disableAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
val errorState = awaitItem()
|
val errorState = awaitItem()
|
||||||
|
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.securebackup.impl.enable
|
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionMode
|
|
||||||
import app.cash.molecule.moleculeFlow
|
|
||||||
import app.cash.turbine.test
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class SecureBackupEnablePresenterTest {
|
|
||||||
@get:Rule
|
|
||||||
val warmUpRule = WarmUpRule()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial state`() = runTest {
|
|
||||||
val presenter = createPresenter()
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.enableAction).isEqualTo(AsyncAction.Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - user enable backup`() = runTest {
|
|
||||||
val presenter = createPresenter()
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink(SecureBackupEnableEvents.EnableBackup)
|
|
||||||
val loadingState = awaitItem()
|
|
||||||
assertThat(loadingState.enableAction).isInstanceOf(AsyncAction.Loading::class.java)
|
|
||||||
val finalState = awaitItem()
|
|
||||||
assertThat(finalState.enableAction).isEqualTo(AsyncAction.Success(Unit))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - user enable backup with error`() = runTest {
|
|
||||||
val encryptionService = FakeEncryptionService()
|
|
||||||
encryptionService.givenEnableBackupsFailure(AN_EXCEPTION)
|
|
||||||
val presenter = createPresenter(encryptionService = encryptionService)
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink(SecureBackupEnableEvents.EnableBackup)
|
|
||||||
val loadingState = awaitItem()
|
|
||||||
assertThat(loadingState.enableAction).isInstanceOf(AsyncAction.Loading::class.java)
|
|
||||||
val errorState = awaitItem()
|
|
||||||
assertThat(errorState.enableAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
|
||||||
errorState.eventSink(SecureBackupEnableEvents.DismissDialog)
|
|
||||||
val finalState = awaitItem()
|
|
||||||
assertThat(finalState.enableAction).isEqualTo(AsyncAction.Uninitialized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPresenter(
|
|
||||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
|
||||||
) = SecureBackupEnablePresenter(
|
|
||||||
encryptionService = encryptionService,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -11,6 +11,7 @@ import app.cash.molecule.RecompositionMode
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
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.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||||
|
|
@ -38,6 +39,8 @@ class SecureBackupRootPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
|
assertThat(initialState.backupState).isEqualTo(BackupState.UNKNOWN)
|
||||||
assertThat(initialState.doesBackupExistOnServer.dataOrNull()).isTrue()
|
assertThat(initialState.doesBackupExistOnServer.dataOrNull()).isTrue()
|
||||||
|
assertThat(initialState.enableAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
assertThat(initialState.displayKeyStorageDisabledError).isFalse()
|
||||||
assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN)
|
assertThat(initialState.recoveryState).isEqualTo(RecoveryState.UNKNOWN)
|
||||||
assertThat(initialState.appName).isEqualTo("Element")
|
assertThat(initialState.appName).isEqualTo("Element")
|
||||||
assertThat(initialState.snackbarMessage).isNull()
|
assertThat(initialState.snackbarMessage).isNull()
|
||||||
|
|
@ -70,6 +73,35 @@ class SecureBackupRootPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - setting up encryption when key storage is disabled should emit a state to render a dialog`() = runTest {
|
||||||
|
val presenter = createSecureBackupRootPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(2)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(SecureBackupRootEvents.DisplayKeyStorageDisabledError)
|
||||||
|
assertThat(awaitItem().displayKeyStorageDisabledError).isTrue()
|
||||||
|
initialState.eventSink(SecureBackupRootEvents.DismissDialog)
|
||||||
|
assertThat(awaitItem().displayKeyStorageDisabledError).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - enable key storage invoke the expected API`() = runTest {
|
||||||
|
val presenter = createSecureBackupRootPresenter()
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
skipItems(2)
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(SecureBackupRootEvents.EnableKeyStorage)
|
||||||
|
assertThat(awaitItem().enableAction.isLoading()).isTrue()
|
||||||
|
assertThat(awaitItem().enableAction.isSuccess()).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createSecureBackupRootPresenter(
|
private fun createSecureBackupRootPresenter(
|
||||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||||
appName: String = "Element",
|
appName: String = "Element",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
* @param trailingContent The content to be displayed after the headline content.
|
* @param trailingContent The content to be displayed after the headline content.
|
||||||
* @param style The style to use for the list item. This may change the color and text styles of the contents. [ListItemStyle.Default] is used by default.
|
* @param style The style to use for the list item. This may change the color and text styles of the contents. [ListItemStyle.Default] is used by default.
|
||||||
* @param enabled Whether the list item is enabled. When disabled, will change the color of the headline content and the leading content to use disabled tokens.
|
* @param enabled Whether the list item is enabled. When disabled, will change the color of the headline content and the leading content to use disabled tokens.
|
||||||
|
* @param alwaysClickable Whether the list item should always be clickable, even when disabled.
|
||||||
* @param onClick The callback to be called when the list item is clicked.
|
* @param onClick The callback to be called when the list item is clicked.
|
||||||
*/
|
*/
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
|
@ -54,6 +55,7 @@ fun ListItem(
|
||||||
trailingContent: ListItemContent? = null,
|
trailingContent: ListItemContent? = null,
|
||||||
style: ListItemStyle = ListItemStyle.Default,
|
style: ListItemStyle = ListItemStyle.Default,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
|
alwaysClickable: Boolean = false,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val colors = ListItemDefaults.colors(
|
val colors = ListItemDefaults.colors(
|
||||||
|
|
@ -74,6 +76,7 @@ fun ListItem(
|
||||||
trailingContent = trailingContent,
|
trailingContent = trailingContent,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
alwaysClickable = alwaysClickable,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +90,7 @@ fun ListItem(
|
||||||
* @param leadingContent The content to be displayed before the headline content.
|
* @param leadingContent The content to be displayed before the headline content.
|
||||||
* @param trailingContent The content to be displayed after the headline content.
|
* @param trailingContent The content to be displayed after the headline content.
|
||||||
* @param enabled Whether the list item is enabled. When disabled, will change the color of the headline content and the leading content to use disabled tokens.
|
* @param enabled Whether the list item is enabled. When disabled, will change the color of the headline content and the leading content to use disabled tokens.
|
||||||
|
* @param alwaysClickable Whether the list item should always be clickable, even when disabled.
|
||||||
* @param onClick The callback to be called when the list item is clicked.
|
* @param onClick The callback to be called when the list item is clicked.
|
||||||
*/
|
*/
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
|
@ -99,6 +103,7 @@ fun ListItem(
|
||||||
leadingContent: ListItemContent? = null,
|
leadingContent: ListItemContent? = null,
|
||||||
trailingContent: ListItemContent? = null,
|
trailingContent: ListItemContent? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
|
alwaysClickable: Boolean = false,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
// We cannot just pass the disabled colors, they must be set manually: https://issuetracker.google.com/issues/280480132
|
// We cannot just pass the disabled colors, they must be set manually: https://issuetracker.google.com/issues/280480132
|
||||||
|
|
@ -149,7 +154,7 @@ fun ListItem(
|
||||||
headlineContent = decoratedHeadlineContent,
|
headlineContent = decoratedHeadlineContent,
|
||||||
modifier = if (onClick != null) {
|
modifier = if (onClick != null) {
|
||||||
Modifier
|
Modifier
|
||||||
.clickable(enabled = enabled, onClick = onClick)
|
.clickable(enabled = enabled || alwaysClickable, onClick = onClick)
|
||||||
.then(modifier)
|
.then(modifier)
|
||||||
} else {
|
} else {
|
||||||
modifier
|
modifier
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3f16fcc4cc994ceeb80df964e9a9c40bf8f85869cd6c11ec14ea327d3b0fa80b
|
oid sha256:48320aed4570138a76b04b08f37f67098a72e1b63f13273fd6d9c0a6e33b7e10
|
||||||
size 38074
|
size 37955
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a432b76372ca2bda9a8f84b8aa779c0eca61e61934ba81d8428e1affdd1f35ae
|
oid sha256:6a71b634518191f299c924f8ddd39b44ddc1255698e981796bb8b034377c515f
|
||||||
size 37818
|
size 37712
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4e8b512463d51570c4645311fe652ee704fe7e67cf2d8f2f6bdf936b987828ea
|
oid sha256:e33a80d4f6dc4a1bd1cd86b2bfd64471926871f92d8d83cae6b32a79459c8ea0
|
||||||
size 38908
|
size 38775
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fdf9185ce8776451ad07410e1cd8169918da9c4734ece14a91f19716d6b3d00d
|
oid sha256:c69e1d5748e65b35525df64b83c6616ab439299284ab2bda3a8408d5f4139d2f
|
||||||
size 38915
|
size 38802
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:259e72d177d2ecab0192cc5be7bdc48c697d4dfca83eb72a1487e83c9e3279a9
|
oid sha256:50d8c9e3272a47de6994d64704c31a4b23840a7ef80b88de7df480b47a0f41d6
|
||||||
size 28889
|
size 32182
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:09f49efb93873a6bffd389ff4edca77bbc00d622669ff25bc5b0c042d65e283f
|
oid sha256:08731d9bad63b2e9fd5e391df69ec8938887e994cd879ddcc0b2d33cd4c53b56
|
||||||
size 27781
|
size 31084
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3081dd73e3a33e786266b49de1267c401e55ac3c5b37d1b9c61749b0f7d55c29
|
oid sha256:9e0c9709139a41288053c9a2cf90359ee41a2a69d70a0b7a0cb0e0aad5e45880
|
||||||
size 99240
|
size 102670
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7eb4d87dd2844f81bf745f53cbe83253fe8b48471692a00dbe0f385de75c8c3a
|
oid sha256:293afc34155d4ecb7ed9f97219b45f7406e3765eb128f512d7dbd3d3cf404e14
|
||||||
size 106079
|
size 109427
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a6fc66d1964b323e7a4215b3127fb5812545116b24c668da7b18e716438b5449
|
oid sha256:2159965425d13676b20c72d1fb1578a708a79b6386ea4ccfc8b8773885f24d40
|
||||||
size 55377
|
size 66188
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5d94b5d9a083d20ddf3d67679fe903234adbf2529aed1d7474c58b22e5bf3d5a
|
oid sha256:2159965425d13676b20c72d1fb1578a708a79b6386ea4ccfc8b8773885f24d40
|
||||||
size 42030
|
size 66188
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9a9646499a3ae2203a941d1b975157820bb96b80bb3107c223e1e4c1b1ceea3f
|
oid sha256:8fadd7226eeb9132a6848635011dd31057e5f9fafc959476500efa4fbe2e907b
|
||||||
size 55957
|
size 66699
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8abb26ff01db7ab458d77f8279f6edf213f6abb9fe0697e7c1984eb3b8602193
|
oid sha256:8149ddbd84689c55b4ef0809a8abbc4686f3e4e15a67b0b1441b3c3ca2b1271d
|
||||||
size 27231
|
size 39939
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:32cdf16ec3d642b0734b8e7829f6733d3c420a4a5d0e86ce78373eac7262af03
|
oid sha256:a9d9919426c607a37e2c42c9c1f11fca097c40accfe2e6b909eb583684aa973b
|
||||||
size 54102
|
size 63709
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b7a7875fa017a8fa65ec7e9509bbddba823ef76fb4c42e023a39e4fe1e1d3b63
|
oid sha256:a9d9919426c607a37e2c42c9c1f11fca097c40accfe2e6b909eb583684aa973b
|
||||||
size 39056
|
size 63709
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:56d99be26704d77d3eca53fda5a74a41e6d69232d73722dd8b6b30d20afee6e6
|
oid sha256:1af579d0d2544971386704109c6332febdf4bc4e6fba79ebb17a7b8b3b0ebc83
|
||||||
size 54676
|
size 64253
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:eb4fd397600880c37e0fa67cacb29e04e4026a239187d6c211509c0084b41103
|
oid sha256:c4ca90b2a211c370707f5aa2f0a3eaa0783c633d595b25bcad658cf283efc1df
|
||||||
size 24750
|
size 37321
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:94acaee1daea87be3ccbbc7372a5473c1554c364323fe487fc8a92a8d5118d22
|
|
||||||
size 12697
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:3c39f29b5435dfd402a046617032f35fd3e8c7ce13337755c67f55b548e19ae8
|
|
||||||
size 13270
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:aec83feacbffef1df6fb3bf3bcf6c937fc95c9e1f8974fdf193868b6fbc4d464
|
|
||||||
size 18222
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:14f1c3d40841097e78385edeb78fba3942545bb52fba33aeac4d5290f636ac05
|
|
||||||
size 12229
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:57e525f8aefe4fa6aba18f46cf67e5ce2c46963ec0820e9f9d575be605b7c95f
|
|
||||||
size 12771
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:c750e2f4038d335de92f98dcdaf4f7a7ca1425ea27266c2fa7867392d9dc9218
|
|
||||||
size 16449
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d97fb4f902a45f03f60c5fbcb9dbcbb1e0699c89832a21a65c940dabf762e167
|
oid sha256:db4b223d8acc7445932515400461ff8c38bf7617f077d96c12291323e1ba7ea9
|
||||||
size 32427
|
size 34930
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d97fb4f902a45f03f60c5fbcb9dbcbb1e0699c89832a21a65c940dabf762e167
|
oid sha256:db4b223d8acc7445932515400461ff8c38bf7617f077d96c12291323e1ba7ea9
|
||||||
size 32427
|
size 34930
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9aaff674eb4d0079c4c66f07657341c311e2d6ff20809b3f3a31f756d496a1f1
|
oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
|
||||||
size 36852
|
size 35760
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:40bdc9bd677b6d4ca459d0aebc66c875450e62138731040bc34c29405bef5d80
|
oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
|
||||||
size 51432
|
size 35760
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2f8aeea3ba0a1aed53900f6451e91b71eab90b991527b1888e1a28e289854730
|
oid sha256:769305f9a2068a58391e9900f0a01f3bf7e38ffaffb452946ad78b1d2d22cf28
|
||||||
size 42049
|
size 55732
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3aefccbfe014fcf17673055d2411c3eee6ec6d52075d9cb3e5c217af6b319750
|
||||||
|
size 54158
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:51546532bdded5aabd353faf1fad1a8fa9e767af62e211d79591528a0bc1726f
|
||||||
|
size 45310
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d2104b99e3ae361dc169c46b0afa00225f8f36dc34d53ac9e2b04136578c6650
|
||||||
|
size 54655
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:bc2628645e4618bf72a8f7d8c2ef33f2150cdae0e606765aa869b42058298493
|
||||||
|
size 41199
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9cf205ea97690e47b25e9b0edb22e004b0d0348d0ca9ddfb677813ee6378e0f9
|
oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
|
||||||
size 33574
|
size 35760
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5a32c172f73d9b9d71f9b134ff79833d7c768d20324c0fda7d1a33c815914b05
|
oid sha256:aa0372c637b7f8c1e83d65a9b037ddcbaf95c940a7974a38bd020489d36245dc
|
||||||
size 33484
|
size 36173
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c144327544cad0a8795205d197bd9c6fb4b735aec0a667a066c48dcc369e8afa
|
oid sha256:5481f4227f3f1cfcca8ff9653c13d43599b60408d6ddaaef854646fca856964c
|
||||||
size 38937
|
size 35153
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9e11e12f7a6b5d91a175280d5ba9e34670856889488e1eb4deed6be39d2e47e5
|
oid sha256:db4b223d8acc7445932515400461ff8c38bf7617f077d96c12291323e1ba7ea9
|
||||||
size 30816
|
size 34930
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9cf205ea97690e47b25e9b0edb22e004b0d0348d0ca9ddfb677813ee6378e0f9
|
oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
|
||||||
size 33574
|
size 35760
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9cf205ea97690e47b25e9b0edb22e004b0d0348d0ca9ddfb677813ee6378e0f9
|
oid sha256:01580b201933b02c99b486f8db6eb039dd41e4cc11e998b4a0dd5dff343b79a0
|
||||||
size 33574
|
size 35030
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9cf205ea97690e47b25e9b0edb22e004b0d0348d0ca9ddfb677813ee6378e0f9
|
oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
|
||||||
size 33574
|
size 35760
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8266c3af58782b0d4d9c6fe9801474f3cfa4c5131d0922a32815c497a2ff8876
|
oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
|
||||||
size 32425
|
size 35760
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9cf205ea97690e47b25e9b0edb22e004b0d0348d0ca9ddfb677813ee6378e0f9
|
oid sha256:d7bbbfe116694102f28a0922586496713a80dcbbdaef327e1249f260d74328a9
|
||||||
size 33574
|
size 35760
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1f925391dfd47629c15b31fe644eb66e43029064aebf73bc7136aa29763cef0a
|
oid sha256:97bafddc5171cf429509e5b16c830460618ef098f78d305658b9f6570fdaec7a
|
||||||
size 31836
|
size 34352
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1f925391dfd47629c15b31fe644eb66e43029064aebf73bc7136aa29763cef0a
|
oid sha256:97bafddc5171cf429509e5b16c830460618ef098f78d305658b9f6570fdaec7a
|
||||||
size 31836
|
size 34352
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:53f3893f337b42d43d85357bc0d779a8bcd7ad51444bd318e885fe01f91392ee
|
oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
|
||||||
size 36089
|
size 35025
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:441bab6f8b50f584cc21d3478872b317995f8547e6078527b69fe3efaff3066e
|
oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
|
||||||
size 50596
|
size 35025
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d5e6a4cba7bd3131c04814ab35ca28c1229ff3d3f6dca66073ac73ee29b00300
|
oid sha256:96e4fc0848f1e59b0146e0f16287f095155216f22112c0aa856d96bee652146a
|
||||||
size 41197
|
size 54887
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a87d5a93849b1395168b8d9f9f3478b62c3219ef8baf6064ce0e321db60823c5
|
||||||
|
size 53363
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a5428124e50f77d5cdcb9309497a07af96f1a3c6f17dbb52a182eb4e04406f3f
|
||||||
|
size 44434
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cfa63bbf8f36cffee2950bbc2c71f0d9a0d599ae21cf41a20fa0e6f9b3183af3
|
||||||
|
size 53824
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:352ded021ffca52ccf5cb947d002da44e2d9d06c0a68f33784c65f35be1d5a4f
|
||||||
|
size 38744
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ad59cb4b2f8d4acb93f71d882ddac39db538bce882e19fc711331550429a2692
|
oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
|
||||||
size 32862
|
size 35025
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3f03f68f065c52756b71e08fac668a263adaaf288bfdb8e4567848ce93540222
|
oid sha256:104d3fe72dcb590e7d452e65b9558f20df3dfad898cfcbab042e6d075c656eb8
|
||||||
size 32879
|
size 35550
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b1e1947a0bb9a7cc951e272952a7811867cda7a9b64aed0fd994ee14bd309f9e
|
oid sha256:807cb5c65c62b5a628ad8719930aecebb03bab9d4d2565aa016a8bb4d23cef58
|
||||||
size 38166
|
size 34593
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0edb47639a0d1338fa5983ab31867a96532dcc174d4d1f6bf7cf646a135fca9f
|
oid sha256:97bafddc5171cf429509e5b16c830460618ef098f78d305658b9f6570fdaec7a
|
||||||
size 30240
|
size 34352
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ad59cb4b2f8d4acb93f71d882ddac39db538bce882e19fc711331550429a2692
|
oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
|
||||||
size 32862
|
size 35025
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ad59cb4b2f8d4acb93f71d882ddac39db538bce882e19fc711331550429a2692
|
oid sha256:4e9940713041c9078087df7e7ceaa430d96ca1c199f4cdc22c705a73539c84a3
|
||||||
size 32862
|
size 32717
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ad59cb4b2f8d4acb93f71d882ddac39db538bce882e19fc711331550429a2692
|
oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
|
||||||
size 32862
|
size 35025
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d8983c85f207fea3ff7d66eb9128dc0c6404d24feae1e102d981e0398414f663
|
oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
|
||||||
size 31842
|
size 35025
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ad59cb4b2f8d4acb93f71d882ddac39db538bce882e19fc711331550429a2692
|
oid sha256:6dfa61ab6f72bd468584dfe25f6aa292269911ef5e31ce6df5e408928be763e9
|
||||||
size 32862
|
size 35025
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3954184d0a03b4a4c2d396e108a4c939d2d07e54eaa59bd886c4b884f423be38
|
oid sha256:76aa1a5ebc4d4700b9c17bd96a860ec66f0f0290174f60bbff421d0621548d43
|
||||||
size 16658
|
size 16369
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fb810bb206a376f1ce0d5c8f13cbcfc6e160a27bbbba01cadb39e217683b47c7
|
oid sha256:5bcd30abc12ac1febb2fd694617d9fe1bd68ff7a193cc05eacc15d1506f3db8e
|
||||||
size 14426
|
size 14129
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:72ffcd02d6e56b3d84ddff56e67bf082cf6867d970e5c90e4c65d7214efc1055
|
oid sha256:a05ec1dbaa4efe16276ef9e284e7eaac194d4ca060524f2fd116465d4f4a5b19
|
||||||
size 16850
|
size 16563
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fb810bb206a376f1ce0d5c8f13cbcfc6e160a27bbbba01cadb39e217683b47c7
|
oid sha256:5bcd30abc12ac1febb2fd694617d9fe1bd68ff7a193cc05eacc15d1506f3db8e
|
||||||
size 14426
|
size 14129
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9ec24e0c0c8ef348b79fca7c884fc04d2959ba3eb68cf86673050e6af5ef2513
|
oid sha256:78fcd3ba4aca3e25a7d183cb83286bd0ba805f48796db034207e60a99dcb20c6
|
||||||
size 16157
|
size 15810
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:319f3ca5dadb404427e6d87046321d7b38bd7b31a085e74d401ede564c083e0f
|
oid sha256:400700ea092e1182d9774f0f717df1085f129fd6c37765d09dff762ffeed9e66
|
||||||
size 13980
|
size 13626
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:df0826b4c857daef4fe641b2dbe52afce3c3e02c74c25d1adbaa7231525c6cae
|
oid sha256:4194ce2cc2259d89ebfaa7dd220bb6ecedf3c0e635a1610d99c2bb2302254ff5
|
||||||
size 16381
|
size 16029
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:319f3ca5dadb404427e6d87046321d7b38bd7b31a085e74d401ede564c083e0f
|
oid sha256:400700ea092e1182d9774f0f717df1085f129fd6c37765d09dff762ffeed9e66
|
||||||
size 13980
|
size 13626
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0837cd46f284fae8d3f6a929b52eb5d7a27f43af7b38ac002a5a36c454998934
|
oid sha256:95f32675a4220b75fd09530db3f97dad1da0b6a5bbc437b740db1da36481de4b
|
||||||
size 43133
|
size 42617
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:63241cb82588d2d3fe27a19de265fb5b0cf5e7d812a783e931ac235df3c64412
|
oid sha256:6901a2702035fc9cbb9e2047844c7644556bee72145f533c41f2cb2cbe166a35
|
||||||
size 40885
|
size 40334
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:000430ba03935ea53fef3c76205d7ea73c72aecc3951974fa67c77235bfaf1a8
|
oid sha256:3dfb18ea5f87f3e8d58c4555a42bbba10b7d4525131c8b40b135a65428a84f09
|
||||||
size 41940
|
size 41465
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:39adceef0bb8ff4ea6386c778f1888d1f1b8f5b3b1f845edab865702a11477de
|
oid sha256:41c4860deea7a74984b997c0f2489bb6aa9f0225f91ea97d3beda42f385e926b
|
||||||
size 39671
|
size 39184
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d890b06789242d9794b168722907c26370a088162d9e435e1a0d3b21a57b01df
|
oid sha256:46554d24e2ccb5a655be40bc79909d2bb3fd04d9fcf914fb8839d915587fff47
|
||||||
size 44451
|
size 44091
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:199e7e41b64c3b16c9ed15de70482babeecffbfe048c75a4740844de7fce0284
|
oid sha256:e925e2547d3d8cb7322897c0a242cc92e658e9dc1b5fa8e5bbc4c3fc3597e4a5
|
||||||
size 42273
|
size 41906
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e53f63a92a5692f395615dba771b0a688a1693e7b715866667a6945d1e349f5d
|
oid sha256:e6e8254736a5a4cdd8e8c52c4b51906700f9c2bff5d656a6797ca3400008cf50
|
||||||
size 43176
|
size 42927
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4637c3b2e4c171389ceabd36a53af376cab9f5f4fe8f99fcc535dff34acf45f9
|
oid sha256:634a2ed76e3b115bb6175fe2801cbef3fb29c284f139a8577c201d1cdea2fc03
|
||||||
size 41088
|
size 40820
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue