Add optimize storage setting and sheet.

This commit is contained in:
Alex Hart
2024-08-01 15:52:51 -03:00
committed by mtang-signal
parent 2677665069
commit 1d1ea01cc1
6 changed files with 288 additions and 7 deletions

View File

@@ -261,6 +261,12 @@ fun MessageBackupsTypeBlock(
style = MaterialTheme.typography.titleMedium
)
val featureIconTint = if (isSelected) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
Column(
verticalArrangement = spacedBy(4.dp),
modifier = Modifier
@@ -268,7 +274,7 @@ fun MessageBackupsTypeBlock(
.padding(horizontal = 16.dp)
) {
messageBackupsType.features.forEach {
MessageBackupsTypeFeatureRow(messageBackupsTypeFeature = it)
MessageBackupsTypeFeatureRow(messageBackupsTypeFeature = it, iconTint = featureIconTint)
}
}
}
@@ -292,7 +298,7 @@ private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
}
}
private fun testBackupTypes(): List<MessageBackupsType> {
fun testBackupTypes(): List<MessageBackupsType> {
return listOf(
MessageBackupsType(
tier = MessageBackupTier.FREE,

View File

@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -67,6 +68,7 @@ import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
import org.thoughtcrime.securesms.preferences.widgets.StorageGraphView
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.viewModel
@@ -79,6 +81,7 @@ class ManageStorageSettingsFragment : ComposeFragment() {
private val viewModel by viewModel<ManageStorageSettingsViewModel> { ManageStorageSettingsViewModel() }
@ExperimentalMaterial3Api
@Composable
override fun FragmentContent() {
val state by viewModel.state.collectAsState()
@@ -102,7 +105,14 @@ class ManageStorageSettingsFragment : ComposeFragment() {
onSetKeepMessages = { navController.navigate("set-keep-messages") },
onSetChatLengthLimit = { navController.navigate("set-chat-length-limit") },
onSyncTrimThreadDeletes = { viewModel.setSyncTrimDeletes(it) },
onDeleteChatHistory = { navController.navigate("confirm-delete-chat-history") }
onDeleteChatHistory = { navController.navigate("confirm-delete-chat-history") },
onToggleOnDeviceStorageOptimization = {
if (state.onDeviceStorageOptimizationState == ManageStorageSettingsViewModel.OnDeviceStorageOptimizationState.REQUIRES_PAID_TIER) {
UpgradeToEnableOptimizedStorageSheet().show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
} else {
viewModel.setOptimizeStorage(it)
}
}
)
}
@@ -236,7 +246,8 @@ private fun ManageStorageSettingsScreen(
onSetKeepMessages: () -> Unit = {},
onSetChatLengthLimit: () -> Unit = {},
onSyncTrimThreadDeletes: (Boolean) -> Unit = {},
onDeleteChatHistory: () -> Unit = {}
onDeleteChatHistory: () -> Unit = {},
onToggleOnDeviceStorageOptimization: (Boolean) -> Unit = {}
) {
Scaffolds.Settings(
title = stringResource(id = R.string.preferences__storage),
@@ -252,6 +263,19 @@ private fun ManageStorageSettingsScreen(
StorageOverview(state.breakdown, onReviewStorage)
if (state.onDeviceStorageOptimizationState > ManageStorageSettingsViewModel.OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE) {
Dividers.Default()
Texts.SectionHeader(text = stringResource(id = R.string.ManageStorageSettingsFragment__on_device_storage))
Rows.ToggleRow(
checked = state.onDeviceStorageOptimizationState == ManageStorageSettingsViewModel.OnDeviceStorageOptimizationState.ENABLED,
text = stringResource(id = R.string.ManageStorageSettingsFragment__optimize_on_device_storage),
label = stringResource(id = R.string.ManageStorageSettingsFragment__unused_media_will_be_offloaded),
onCheckChanged = onToggleOnDeviceStorageOptimization
)
}
Dividers.Default()
Texts.SectionHeader(text = stringResource(id = R.string.ManageStorageSettingsFragment_chat_limit))
@@ -510,7 +534,8 @@ private fun ManageStorageSettingsScreenPreview() {
ManageStorageSettingsScreen(
state = ManageStorageSettingsViewModel.ManageStorageState(
keepMessagesDuration = KeepMessagesDuration.FOREVER,
lengthLimit = ManageStorageSettingsViewModel.ManageStorageState.NO_LIMIT
lengthLimit = ManageStorageSettingsViewModel.ManageStorageState.NO_LIMIT,
onDeviceStorageOptimizationState = ManageStorageSettingsViewModel.OnDeviceStorageOptimizationState.DISABLED
)
)
}

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.database.MediaTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.media
@@ -20,6 +21,7 @@ import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
class ManageStorageSettingsViewModel : ViewModel() {
@@ -27,7 +29,8 @@ class ManageStorageSettingsViewModel : ViewModel() {
ManageStorageState(
keepMessagesDuration = SignalStore.settings.keepMessagesDuration,
lengthLimit = if (SignalStore.settings.isTrimByLengthEnabled) SignalStore.settings.threadTrimLength else ManageStorageState.NO_LIMIT,
syncTrimDeletes = SignalStore.settings.shouldSyncThreadTrimDeletes()
syncTrimDeletes = SignalStore.settings.shouldSyncThreadTrimDeletes(),
onDeviceStorageOptimizationState = getOnDeviceStorageOptimizationState()
)
)
val state = store.asStateFlow()
@@ -88,16 +91,56 @@ class ManageStorageSettingsViewModel : ViewModel() {
store.update { it.copy(syncTrimDeletes = syncTrimDeletes) }
}
fun setOptimizeStorage(enabled: Boolean) {
val storageState = getOnDeviceStorageOptimizationState()
if (storageState >= OnDeviceStorageOptimizationState.DISABLED) {
SignalStore.backup.optimizeStorage = enabled
store.update { it.copy(onDeviceStorageOptimizationState = if (enabled) OnDeviceStorageOptimizationState.ENABLED else OnDeviceStorageOptimizationState.DISABLED) }
}
}
private fun isRestrictingLengthLimitChange(newLimit: Int): Boolean {
return state.value.lengthLimit == ManageStorageState.NO_LIMIT || (newLimit != ManageStorageState.NO_LIMIT && newLimit < state.value.lengthLimit)
}
private fun getOnDeviceStorageOptimizationState(): OnDeviceStorageOptimizationState {
return when {
!RemoteConfig.messageBackups -> OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE
!SignalStore.backup.areBackupsEnabled || SignalStore.backup.backupTier != MessageBackupTier.PAID -> OnDeviceStorageOptimizationState.REQUIRES_PAID_TIER
SignalStore.backup.optimizeStorage -> OnDeviceStorageOptimizationState.ENABLED
else -> OnDeviceStorageOptimizationState.DISABLED
}
}
enum class OnDeviceStorageOptimizationState {
/**
* The entire feature is not available and the option should not be displayed to the user.
*/
FEATURE_NOT_AVAILABLE,
/**
* The feature is available, but the user is not on the paid backups plan.
*/
REQUIRES_PAID_TIER,
/**
* The user is on the paid backups plan but optimized storage is disabled.
*/
DISABLED,
/**
* The user is on the paid backups plan and optimized storage is enabled.
*/
ENABLED
}
@Immutable
data class ManageStorageState(
val keepMessagesDuration: KeepMessagesDuration = KeepMessagesDuration.FOREVER,
val lengthLimit: Int = NO_LIMIT,
val syncTrimDeletes: Boolean = true,
val breakdown: MediaTable.StorageBreakdown? = null
val breakdown: MediaTable.StorageBreakdown? = null,
val onDeviceStorageOptimizationState: OnDeviceStorageOptimizationState
) {
companion object {
const val NO_LIMIT = 0

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.storage
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Icons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeBlock
import org.thoughtcrime.securesms.backup.v2.ui.subscription.testBackupTypes
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
/**
* Sheet describing how users must upgrade to enable optimized storage.
*/
class UpgradeToEnableOptimizedStorageSheet : ComposeBottomSheetDialogFragment() {
override val peekHeightPercentage: Float = 1f
private val viewModel: UpgradeToEnableOptimizedStorageViewModel by viewModels()
@Composable
override fun SheetContent() {
val type by viewModel.messageBackupsType
UpgradeToEnableOptimizedStorageSheetContent(
messageBackupsType = type,
onUpgradeNowClick = {
startActivity(CheckoutFlowActivity.createIntent(requireContext(), InAppPaymentType.RECURRING_BACKUP))
dismissAllowingStateLoss()
},
onCancelClick = {
dismissAllowingStateLoss()
}
)
}
}
@Composable
private fun UpgradeToEnableOptimizedStorageSheetContent(
messageBackupsType: MessageBackupsType?,
onUpgradeNowClick: () -> Unit = {},
onCancelClick: () -> Unit = {}
) {
if (messageBackupsType == null) {
// TODO [message-backups] -- network error?
return
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
BottomSheets.Handle()
Icons.BrushedForeground(
painter = painterResource(id = R.drawable.symbol_backup_light),
contentDescription = null,
foregroundBrush = BackupsIconColors.Normal.foreground,
modifier = Modifier
.padding(top = 8.dp, bottom = 12.dp)
.size(88.dp)
.background(
color = BackupsIconColors.Normal.background,
shape = CircleShape
)
.padding(20.dp)
)
Text(
text = stringResource(id = R.string.UpgradeToEnableOptimizedStorageSheet__upgrade_to_enable_this_feature),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
)
Text(
text = stringResource(id = R.string.UpgradeToEnableOptimizedStorageSheet__storage_optimization_can_only_be_used),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 10.dp, bottom = 28.dp)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
)
MessageBackupsTypeBlock(
messageBackupsType = messageBackupsType,
isCurrent = false,
isSelected = false,
onSelected = {},
enabled = false,
modifier = Modifier
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(bottom = 50.dp)
)
Buttons.LargePrimary(
onClick = onUpgradeNowClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(bottom = 8.dp)
) {
Text(
text = stringResource(id = R.string.UpgradeToEnableOptimizedStorageSheet__upgrade_now)
)
}
TextButton(
onClick = onCancelClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(bottom = 16.dp)
) {
Text(
text = stringResource(id = android.R.string.cancel)
)
}
}
}
@SignalPreview
@Composable
private fun UpgradeToEnableOptimizedStorageSheetContentPreview() {
Previews.BottomSheetPreview {
UpgradeToEnableOptimizedStorageSheetContent(
messageBackupsType = testBackupTypes()[1]
)
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.storage
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
class UpgradeToEnableOptimizedStorageViewModel : ViewModel() {
private val internalMessageBackupsType = mutableStateOf<MessageBackupsType?>(null)
val messageBackupsType: State<MessageBackupsType?> = internalMessageBackupsType
init {
viewModelScope.launch {
val backupsType = withContext(Dispatchers.IO) {
BackupRepository.getBackupsType(MessageBackupTier.PAID)
}
withContext(Dispatchers.Main) {
internalMessageBackupsType.value = backupsType
}
}
}
}

View File

@@ -7217,6 +7217,20 @@
<string name="ManageStorageSettingsFragment_apply_limits_title">Apply limits to linked devices</string>
<!-- Setting description for syncing automated chat limit trimming (deleting messages automatically by length or date) to linked devices -->
<string name="ManageStorageSettingsFragment_apply_limits_description">When enabled, chat limits will also delete messages from your linked devices.</string>
<!-- Setting section title header for storage optimization -->
<string name="ManageStorageSettingsFragment__on_device_storage">On-device storage</string>
<!-- Setting row title for storage optimization -->
<string name="ManageStorageSettingsFragment__optimize_on_device_storage">Optimize on-device storage</string>
<!-- Setting row explanation for storage optimization -->
<string name="ManageStorageSettingsFragment__unused_media_will_be_offloaded">Unused media will be offloaded, but can be downloaded from your backup anytime.</string>
<!-- UpgradeToEnableOptimizedStorageSheet -->
<!-- Title on a bottom sheet, detailing that the user must upgrade to enable storage optimization -->
<string name="UpgradeToEnableOptimizedStorageSheet__upgrade_to_enable_this_feature">Upgrade to enable this feature</string>
<!-- Subtitle of bottom sheet detailing that the user must upgrade to enable storage optimization -->
<string name="UpgradeToEnableOptimizedStorageSheet__storage_optimization_can_only_be_used">Storage optimization can only be used with the paid tier of Signal Backups. Upgrade your backup plan to start using this feature.</string>
<!-- Primary action to launch upgrade flow -->
<string name="UpgradeToEnableOptimizedStorageSheet__upgrade_now">Upgrade now</string>
<!-- Educational bottom sheet dialog title shown to notify about delete syncs causing deletes to happen across all devices -->
<string name="DeleteSyncEducation_title">Deleting is now synced across all of your devices</string>