Stub out MoreOptionsSheet and RestoreFromBackupFragment.

This commit is contained in:
Alex Hart
2024-03-21 17:17:05 -03:00
committed by Nicholas Tinsley
parent 7802448b24
commit 5f5a80dcbe
13 changed files with 756 additions and 39 deletions

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.devicetransfer.moreoptions
/**
* Allows component opening sheet to specify mode
*/
enum class MoreTransferOrRestoreOptionsMode {
/**
* Only display the option to log in without transferring. Selection
* will be disabled.
*/
SKIP_ONLY,
/**
* Display transfer/restore local/skip as well as a next and cancel button
*/
SELECTION
}

View File

@@ -0,0 +1,338 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.devicetransfer.moreoptions
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
/**
* Lists a set of options the user can choose from for restoring backup or skipping restoration
*/
class MoreTransferOrRestoreOptionsSheet : ComposeBottomSheetDialogFragment() {
private val args by navArgs<MoreTransferOrRestoreOptionsSheetArgs>()
@Composable
override fun SheetContent() {
var selectedOption by remember {
mutableStateOf<BackupRestorationType?>(null)
}
MoreOptionsSheetContent(
mode = args.mode,
selectedOption = selectedOption,
onOptionSelected = { selectedOption = it },
onCancelClick = { findNavController().popBackStack() },
onNextClick = {
this.onNextClicked(selectedOption ?: BackupRestorationType.NONE)
}
)
}
private fun onNextClicked(selectedOption: BackupRestorationType) {
// TODO [message-requests] -- Launch next screen based off user choice
}
}
@Preview
@Composable
private fun MoreOptionsSheetContentPreview() {
Previews.BottomSheetPreview {
MoreOptionsSheetContent(
mode = MoreTransferOrRestoreOptionsMode.SKIP_ONLY,
selectedOption = null,
onOptionSelected = {},
onCancelClick = {},
onNextClick = {}
)
}
}
@Preview
@Composable
private fun MoreOptionsSheetSelectableContentPreview() {
Previews.BottomSheetPreview {
MoreOptionsSheetContent(
mode = MoreTransferOrRestoreOptionsMode.SELECTION,
selectedOption = null,
onOptionSelected = {},
onCancelClick = {},
onNextClick = {}
)
}
}
@Composable
private fun MoreOptionsSheetContent(
mode: MoreTransferOrRestoreOptionsMode,
selectedOption: BackupRestorationType?,
onOptionSelected: (BackupRestorationType) -> Unit,
onCancelClick: () -> Unit,
onNextClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
BottomSheets.Handle()
Spacer(modifier = Modifier.size(42.dp))
if (mode == MoreTransferOrRestoreOptionsMode.SELECTION) {
TransferFromAndroidDeviceOption(
selectedOption = selectedOption,
onOptionSelected = onOptionSelected
)
Spacer(modifier = Modifier.size(16.dp))
RestoreLocalBackupOption(
selectedOption = selectedOption,
onOptionSelected = onOptionSelected
)
Spacer(modifier = Modifier.size(16.dp))
}
LogInWithoutTransferringOption(
selectedOption = selectedOption,
onOptionSelected = when (mode) {
MoreTransferOrRestoreOptionsMode.SKIP_ONLY -> { _ -> onNextClick() }
MoreTransferOrRestoreOptionsMode.SELECTION -> onOptionSelected
}
)
if (mode == MoreTransferOrRestoreOptionsMode.SELECTION) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 30.dp, bottom = 24.dp)
) {
TextButton(
onClick = onCancelClick
) {
Text(text = stringResource(id = android.R.string.cancel))
}
Spacer(modifier = Modifier.weight(1f))
Buttons.LargeTonal(
enabled = selectedOption != null,
onClick = onNextClick
) {
Text(text = stringResource(id = R.string.RegistrationActivity_next))
}
}
} else {
Spacer(modifier = Modifier.size(45.dp))
}
}
}
@Preview
@Composable
private fun LogInWithoutTransferringOptionPreview() {
Previews.BottomSheetPreview {
LogInWithoutTransferringOption(
selectedOption = null,
onOptionSelected = {}
)
}
}
@Composable
private fun LogInWithoutTransferringOption(
selectedOption: BackupRestorationType?,
onOptionSelected: (BackupRestorationType) -> Unit
) {
Option(
icon = {
Box(
modifier = Modifier.padding(horizontal = 18.dp)
) {
Icon(
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset.
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(36.dp)
)
}
},
isSelected = selectedOption == BackupRestorationType.NONE,
title = "Log in without transferring", // TODO [message-backups] Finalized copy.
subtitle = "Continue without transferring your messages and media", // TODO [message-backups] Finalized copy.
onClick = { onOptionSelected(BackupRestorationType.NONE) }
)
}
@Preview
@Composable
private fun TransferFromAndroidDeviceOptionPreview() {
Previews.BottomSheetPreview {
TransferFromAndroidDeviceOption(
selectedOption = null,
onOptionSelected = {}
)
}
}
@Composable
private fun TransferFromAndroidDeviceOption(
selectedOption: BackupRestorationType?,
onOptionSelected: (BackupRestorationType) -> Unit
) {
Option(
icon = {
Box(
modifier = Modifier.padding(horizontal = 18.dp)
) {
Icon(
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset.
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(36.dp)
)
}
},
isSelected = selectedOption == BackupRestorationType.DEVICE_TRANSFER,
title = "Transfer from Android device", // TODO [message-backups] Finalized copy.
subtitle = "Transfer your account and messages from your old device.", // TODO [message-backups] Finalized copy.
onClick = { onOptionSelected(BackupRestorationType.DEVICE_TRANSFER) }
)
}
@Preview
@Composable
private fun RestoreLocalBackupOptionPreview() {
Previews.BottomSheetPreview {
RestoreLocalBackupOption(
selectedOption = null,
onOptionSelected = {}
)
}
}
@Composable
private fun RestoreLocalBackupOption(
selectedOption: BackupRestorationType?,
onOptionSelected: (BackupRestorationType) -> Unit
) {
Option(
icon = {
Box(
modifier = Modifier.padding(horizontal = 18.dp)
) {
Icon(
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset.
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(36.dp)
)
}
},
isSelected = selectedOption == BackupRestorationType.LOCAL_BACKUP,
title = "Restore local backup", // TODO [message-backups] Finalized copy.
subtitle = "Restore your messages from a backup file you saved on your device.", // TODO [message-backups] Finalized copy.
onClick = { onOptionSelected(BackupRestorationType.LOCAL_BACKUP) }
)
}
@Preview
@Composable
private fun OptionPreview() {
Previews.BottomSheetPreview {
Option(
icon = {
Box(
modifier = Modifier.padding(horizontal = 18.dp)
) {
Icon(
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset.
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(36.dp)
)
}
},
isSelected = false,
title = "Log in without transferring", // TODO [message-backups] Finalized copy.
subtitle = "Continue without transferring your messages and media", // TODO [message-backups] Finalized copy.
onClick = {}
)
}
}
@Composable
private fun Option(
icon: @Composable () -> Unit,
isSelected: Boolean,
title: String,
subtitle: String,
onClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(12.dp)
)
.border(
width = if (isSelected) 2.dp else 0.dp,
color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent
)
.clip(RoundedCornerShape(12.dp))
.clickable { onClick() }
.padding(vertical = 21.dp)
) {
icon()
Column {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.devicetransfer.newdevice
/**
* What kind of backup restore the user wishes to perform.
*/
enum class BackupRestorationType {
DEVICE_TRANSFER,
LOCAL_BACKUP,
REMOTE_BACKUP,
NONE
}

View File

@@ -12,6 +12,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.databinding.FragmentTransferRestoreBinding;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
@@ -36,7 +37,13 @@ public final class TransferOrRestoreFragment extends LoggingFragment {
binding.transferOrRestoreFragmentTransfer.setOnClickListener(v -> viewModel.onTransferFromAndroidDeviceSelected());
binding.transferOrRestoreFragmentRestore.setOnClickListener(v -> viewModel.onRestoreFromLocalBackupSelected());
binding.transferOrRestoreFragmentRestoreRemote.setOnClickListener(v -> viewModel.onRestoreFromRemoteBackupSelected());
binding.transferOrRestoreFragmentNext.setOnClickListener(v -> launchSelection(viewModel.getStateSnapshot()));
binding.transferOrRestoreFragmentMoreOptions.setOnClickListener(v -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_transferOrRestore_to_moreOptions));
int visibility = FeatureFlags.messageBackups() ? View.VISIBLE : View.GONE;
binding.transferOrRestoreFragmentRestoreRemoteCard.setVisibility(visibility);
binding.transferOrRestoreFragmentMoreOptions.setVisibility(visibility);
String description = getString(R.string.TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device);
String toBold = getString(R.string.TransferOrRestoreFragment__you_need_access_to_your_old_device);
@@ -47,15 +54,18 @@ public final class TransferOrRestoreFragment extends LoggingFragment {
lifecycleDisposable.add(viewModel.getState().subscribe(this::updateSelection));
}
private void updateSelection(TransferOrRestoreViewModel.RestorationType restorationType) {
binding.transferOrRestoreFragmentTransferCard.setSelected(restorationType == TransferOrRestoreViewModel.RestorationType.DEVICE_TRANSFER);
binding.transferOrRestoreFragmentRestoreCard.setSelected(restorationType == TransferOrRestoreViewModel.RestorationType.LOCAL_BACKUP);
private void updateSelection(BackupRestorationType restorationType) {
binding.transferOrRestoreFragmentTransferCard.setSelected(restorationType == BackupRestorationType.DEVICE_TRANSFER);
binding.transferOrRestoreFragmentRestoreCard.setSelected(restorationType == BackupRestorationType.LOCAL_BACKUP);
binding.transferOrRestoreFragmentRestoreRemoteCard.setSelected(restorationType == BackupRestorationType.REMOTE_BACKUP);
}
private void launchSelection(TransferOrRestoreViewModel.RestorationType restorationType) {
private void launchSelection(BackupRestorationType restorationType) {
switch (restorationType) {
case DEVICE_TRANSFER -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_new_device_transfer_instructions);
case LOCAL_BACKUP -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_choose_backup);
case REMOTE_BACKUP -> {}
default -> throw new IllegalArgumentException();
}
}
}

View File

@@ -15,21 +15,20 @@ import io.reactivex.rxjava3.processors.BehaviorProcessor
*/
class TransferOrRestoreViewModel : ViewModel() {
private val internalState = BehaviorProcessor.createDefault(RestorationType.DEVICE_TRANSFER)
private val internalState = BehaviorProcessor.createDefault(BackupRestorationType.DEVICE_TRANSFER)
val state: Flowable<RestorationType> = internalState.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread())
val stateSnapshot: RestorationType get() = internalState.value!!
val state: Flowable<BackupRestorationType> = internalState.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread())
val stateSnapshot: BackupRestorationType get() = internalState.value!!
fun onTransferFromAndroidDeviceSelected() {
internalState.onNext(RestorationType.DEVICE_TRANSFER)
internalState.onNext(BackupRestorationType.DEVICE_TRANSFER)
}
fun onRestoreFromLocalBackupSelected() {
internalState.onNext(RestorationType.LOCAL_BACKUP)
internalState.onNext(BackupRestorationType.LOCAL_BACKUP)
}
enum class RestorationType {
DEVICE_TRANSFER,
LOCAL_BACKUP
fun onRestoreFromRemoteBackupSelected() {
internalState.onNext(BackupRestorationType.REMOTE_BACKUP)
}
}