mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 09:49:30 +01:00
Polish deletion UX.
This commit is contained in:
committed by
Cody Henthorne
parent
ccce37d023
commit
df170dac32
@@ -50,7 +50,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -275,6 +274,10 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
||||
override fun onRedemptionErrorDetailsClick() {
|
||||
BackupAlertBottomSheet.create(BackupAlert.CouldNotRedeemBackup).show(parentFragmentManager, null)
|
||||
}
|
||||
|
||||
override fun onDisplayProgressDialog() {
|
||||
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER)
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayBackupKey() {
|
||||
@@ -361,6 +364,7 @@ private interface ContentCallbacks {
|
||||
fun onRestoreUsingCellularConfirm() = Unit
|
||||
fun onRestoreUsingCellularClick() = Unit
|
||||
fun onRedemptionErrorDetailsClick() = Unit
|
||||
fun onDisplayProgressDialog() = Unit
|
||||
|
||||
object Emtpy : ContentCallbacks
|
||||
}
|
||||
@@ -390,6 +394,12 @@ private fun RemoteBackupsSettingsContent(
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
LaunchedEffect(backupDeleteState) {
|
||||
if (backupDeleteState != DeletionState.NONE && backupDeleteState != DeletionState.CLEAR_LOCAL_STATE) {
|
||||
contentCallbacks.onDialogDismissed()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Scaffolds.DefaultTopAppBar(
|
||||
@@ -449,7 +459,7 @@ private fun RemoteBackupsSettingsContent(
|
||||
state = backupState,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
|
||||
onRenewClick = contentCallbacks::onRenewLostSubscription,
|
||||
isRenewEnabled = backupDeleteState != DeletionState.RUNNING
|
||||
isRenewEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS
|
||||
)
|
||||
}
|
||||
|
||||
@@ -459,7 +469,7 @@ private fun RemoteBackupsSettingsContent(
|
||||
BackupCard(
|
||||
backupState = backupState,
|
||||
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick,
|
||||
buttonsEnabled = backupDeleteState != DeletionState.RUNNING
|
||||
buttonsEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS
|
||||
)
|
||||
}
|
||||
|
||||
@@ -468,14 +478,19 @@ private fun RemoteBackupsSettingsContent(
|
||||
title = stringResource(R.string.RemoteBackupsSettingsFragment__your_subscription_was_not_found),
|
||||
onRenewClick = contentCallbacks::onRenewLostSubscription,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
|
||||
isRenewEnabled = backupDeleteState != DeletionState.RUNNING
|
||||
isRenewEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (backupDeleteState != DeletionState.NONE) {
|
||||
appendBackupDeletionState(backupDeleteState, contentCallbacks)
|
||||
if (backupDeleteState != DeletionState.NONE && backupDeleteState != DeletionState.CLEAR_LOCAL_STATE) {
|
||||
appendBackupDeletionItems(
|
||||
backupDeleteState = backupDeleteState,
|
||||
backupRestoreState = backupRestoreState,
|
||||
canRestoreUsingCellular = canRestoreUsingCellular,
|
||||
contentCallbacks = contentCallbacks
|
||||
)
|
||||
} else if (backupsEnabled) {
|
||||
appendBackupDetailsItems(
|
||||
backupState = backupState,
|
||||
@@ -619,22 +634,42 @@ private fun ReenableBackupsButton(contentCallbacks: ContentCallbacks) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.appendBackupDeletionState(
|
||||
private fun LazyListScope.appendRestoreFromBackupStatusData(
|
||||
backupRestoreState: BackupRestoreState.FromBackupStatusData,
|
||||
canRestoreUsingCellular: Boolean,
|
||||
contentCallbacks: ContentCallbacks
|
||||
) {
|
||||
item {
|
||||
BackupStatusRow(
|
||||
backupStatusData = backupRestoreState.backupStatusData,
|
||||
onCancelClick = contentCallbacks::onCancelMediaRestore,
|
||||
onSkipClick = contentCallbacks::onSkipMediaRestore,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
|
||||
)
|
||||
}
|
||||
|
||||
if (!canRestoreUsingCellular) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__resume_download),
|
||||
icon = painterResource(R.drawable.symbol_arrow_circle_down_24),
|
||||
onClick = contentCallbacks::onRestoreUsingCellularConfirm
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.appendBackupDeletionItems(
|
||||
backupDeleteState: DeletionState,
|
||||
backupRestoreState: BackupRestoreState,
|
||||
canRestoreUsingCellular: Boolean,
|
||||
contentCallbacks: ContentCallbacks
|
||||
) {
|
||||
when (backupDeleteState) {
|
||||
DeletionState.NONE -> return
|
||||
DeletionState.FAILED -> {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_but_there_was_an_error),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp)
|
||||
)
|
||||
DescriptionText(text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_but_there_was_an_error))
|
||||
}
|
||||
|
||||
item {
|
||||
@@ -665,18 +700,33 @@ private fun LazyListScope.appendBackupDeletionState(
|
||||
}
|
||||
}
|
||||
|
||||
DeletionState.RUNNING -> {
|
||||
DeletionState.AWAITING_MEDIA_DOWNLOAD -> {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_and_your_data),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp)
|
||||
DescriptionText(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_your_data_will_be)
|
||||
)
|
||||
}
|
||||
|
||||
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
|
||||
appendRestoreFromBackupStatusData(
|
||||
backupRestoreState = backupRestoreState,
|
||||
canRestoreUsingCellular = canRestoreUsingCellular,
|
||||
contentCallbacks = contentCallbacks
|
||||
)
|
||||
} else {
|
||||
item {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeletionState.DELETE_BACKUPS -> {
|
||||
item {
|
||||
DescriptionText(text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_and_your_data))
|
||||
}
|
||||
|
||||
item {
|
||||
Column(
|
||||
verticalArrangement = spacedBy(12.dp),
|
||||
@@ -696,9 +746,40 @@ private fun LazyListScope.appendBackupDeletionState(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DeletionState.COMPLETE -> {
|
||||
item {
|
||||
DescriptionText(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_and_your_data_has_been_deleted),
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
ReenableBackupsButton(contentCallbacks)
|
||||
}
|
||||
}
|
||||
|
||||
DeletionState.CLEAR_LOCAL_STATE -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DescriptionText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 16.dp)
|
||||
.then(modifier)
|
||||
)
|
||||
}
|
||||
|
||||
private fun LazyListScope.appendBackupDetailsItems(
|
||||
backupState: RemoteBackupsSettingsState.BackupState,
|
||||
backupRestoreState: BackupRestoreState,
|
||||
@@ -720,24 +801,11 @@ private fun LazyListScope.appendBackupDetailsItems(
|
||||
|
||||
if (backupRestoreState !is BackupRestoreState.None) {
|
||||
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
|
||||
item {
|
||||
BackupStatusRow(
|
||||
backupStatusData = backupRestoreState.backupStatusData,
|
||||
onCancelClick = contentCallbacks::onCancelMediaRestore,
|
||||
onSkipClick = contentCallbacks::onSkipMediaRestore,
|
||||
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
|
||||
)
|
||||
}
|
||||
|
||||
if (!canRestoreUsingCellular) {
|
||||
item {
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__resume_download),
|
||||
icon = painterResource(R.drawable.symbol_arrow_circle_down_24),
|
||||
onClick = contentCallbacks::onRestoreUsingCellularConfirm
|
||||
)
|
||||
}
|
||||
}
|
||||
appendRestoreFromBackupStatusData(
|
||||
backupRestoreState = backupRestoreState,
|
||||
canRestoreUsingCellular = canRestoreUsingCellular,
|
||||
contentCallbacks = contentCallbacks
|
||||
)
|
||||
} else if (backupRestoreState is BackupRestoreState.Ready) {
|
||||
item {
|
||||
BackupReadyToDownloadRow(
|
||||
@@ -1363,7 +1431,7 @@ private fun TurnOffAndDeleteBackupsDialog(
|
||||
) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(id = R.string.RemoteBackupsSettingsFragment__turn_off_and_delete_backups),
|
||||
body = stringResource(id = R.string.RemoteBackupsSettingsFragment__you_will_not_be_charged_again),
|
||||
body = stringResource(id = R.string.RemoteBackupsSettingsFragment__your_backup_will_be_deleted_and_no_new_backups_will_be_created),
|
||||
confirm = stringResource(id = R.string.RemoteBackupsSettingsFragment__turn_off_and_delete),
|
||||
dismiss = stringResource(id = android.R.string.cancel),
|
||||
confirmColor = MaterialTheme.colorScheme.error,
|
||||
@@ -1879,28 +1947,31 @@ private fun BackupFrequencyDialogPreview() {
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupDeletionCardPreview() {
|
||||
var backupDeletionState by remember { mutableStateOf(DeletionState.NONE) }
|
||||
|
||||
Previews.Preview {
|
||||
LazyColumn {
|
||||
item {
|
||||
Buttons.MediumTonal(
|
||||
onClick = {
|
||||
backupDeletionState = when (backupDeletionState) {
|
||||
DeletionState.FAILED -> DeletionState.NONE
|
||||
DeletionState.NONE -> DeletionState.RUNNING
|
||||
DeletionState.RUNNING -> DeletionState.FAILED
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Next Deletion State")
|
||||
for (state in DeletionState.entries.filter { it.hasUx() }) {
|
||||
appendBackupDeletionItems(
|
||||
backupDeleteState = state,
|
||||
backupRestoreState = BackupRestoreState.FromBackupStatusData(
|
||||
backupStatusData = BackupStatusData.RestoringMedia(
|
||||
bytesDownloaded = 80.mebiBytes,
|
||||
bytesTotal = 3.gibiBytes
|
||||
)
|
||||
),
|
||||
contentCallbacks = ContentCallbacks.Emtpy,
|
||||
canRestoreUsingCellular = true
|
||||
)
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
}
|
||||
appendBackupDeletionState(backupDeletionState, contentCallbacks = ContentCallbacks.Emtpy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DeletionState.hasUx() = this !in setOf(DeletionState.NONE, DeletionState.CLEAR_LOCAL_STATE)
|
||||
|
||||
private fun ArchiveUploadProgressState.frameExportProgress(): Float {
|
||||
return if (this.frameTotalCount == 0L) {
|
||||
0f
|
||||
|
||||
@@ -14,7 +14,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -47,6 +46,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||
import org.thoughtcrime.securesms.service.MessageBackupListener
|
||||
import java.util.Currency
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
@@ -78,11 +78,9 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
SignalStore.backup.deletionStateFlow
|
||||
.filter { it == DeletionState.NONE }
|
||||
.collect {
|
||||
refresh()
|
||||
}
|
||||
SignalStore.backup.deletionStateFlow.collectLatest {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
@@ -164,6 +162,10 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
fun skipMediaRestore() {
|
||||
BackupRepository.skipMediaRestore()
|
||||
|
||||
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
|
||||
BackupRepository.continueTurningOffAndDisablingBackups()
|
||||
}
|
||||
}
|
||||
|
||||
fun requestDialog(dialog: RemoteBackupsSettingsState.Dialog) {
|
||||
@@ -187,6 +189,8 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun turnOffAndDeleteBackups() {
|
||||
requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER)
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
BackupRepository.turnOffAndDisableBackups()
|
||||
}
|
||||
@@ -202,6 +206,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) {
|
||||
try {
|
||||
Log.i(TAG, "Performing a state refresh.")
|
||||
performStateRefresh(lastPurchase)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "State refresh failed", e)
|
||||
@@ -241,7 +246,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] A mismatch was detected.")
|
||||
|
||||
val hasActiveGooglePlayBillingSubscription = when (val purchaseResult = AppDependencies.billingApi.queryPurchases()) {
|
||||
is BillingPurchaseResult.Success -> purchaseResult.isAcknowledged && purchaseResult.isWithinTheLastMonth()
|
||||
is BillingPurchaseResult.Success -> purchaseResult.isAcknowledged && purchaseResult.isWithinTheLastMonth() && purchaseResult.isAutoRenewing
|
||||
else -> false
|
||||
} || SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID
|
||||
|
||||
@@ -339,6 +344,15 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
backupState = RemoteBackupsSettingsState.BackupState.NotFound
|
||||
)
|
||||
}
|
||||
} else if (lastPurchase != null && lastPurchase.endOfPeriod > System.currentTimeMillis().milliseconds) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
backupState = RemoteBackupsSettingsState.BackupState.Canceled(
|
||||
messageBackupsType = type,
|
||||
renewalTime = lastPurchase.endOfPeriod
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
|
||||
Reference in New Issue
Block a user