Add Delete for Me sync support.

This commit is contained in:
Cody Henthorne
2024-05-21 15:11:06 -04:00
parent 1c66da7873
commit a81a675d59
40 changed files with 2274 additions and 198 deletions

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.FragmentManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.subjects.CompletableSubject
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.TextSecurePreferences
/**
* Show educational info about delete syncing to linked devices. This dialog uses a subject to convey when
* it completes and will dismiss itself if that subject is null aka dialog is recreated by OS instead of being
* shown by our code.
*/
class DeleteSyncEducationDialog : ComposeBottomSheetDialogFragment() {
companion object {
@JvmStatic
fun shouldShow(): Boolean {
return TextSecurePreferences.isMultiDevice(ApplicationDependencies.getApplication()) &&
!SignalStore.uiHints().hasSeenDeleteSyncEducationSheet &&
FeatureFlags.deleteSyncEnabled()
}
@JvmStatic
fun show(fragmentManager: FragmentManager): Completable {
val dialog = DeleteSyncEducationDialog()
dialog.show(fragmentManager, null)
SignalStore.uiHints().hasSeenDeleteSyncEducationSheet = true
val subject = CompletableSubject.create()
dialog.subject = subject
return subject
.onErrorComplete()
.observeOn(AndroidSchedulers.mainThread())
}
}
override val peekHeightPercentage: Float = 1f
private var subject: CompletableSubject? = null
@Composable
override fun SheetContent() {
Sheet(dismiss = this::dismissAllowingStateLoss)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (subject == null || savedInstanceState != null) {
dismissAllowingStateLoss()
}
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
subject?.onComplete()
}
}
@Composable
private fun Sheet(
dismiss: () -> Unit = {}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(24.dp)
) {
Image(
painter = painterResource(id = R.drawable.delete_sync),
contentDescription = null,
modifier = Modifier
.padding(top = 48.dp)
)
Text(
text = stringResource(id = R.string.DeleteSyncEducation_title),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 24.dp, bottom = 12.dp)
)
Text(
text = stringResource(id = R.string.DeleteSyncEducation_message),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.secondary
)
Buttons.LargeTonal(
onClick = dismiss,
modifier = Modifier
.padding(top = 64.dp)
.defaultMinSize(minWidth = 132.dp)
) {
Text(text = stringResource(id = R.string.DeleteSyncEducation_acknowledge_button))
}
}
}
@SignalPreview
@Composable
private fun SheetPreview() {
Previews.Preview {
Sheet()
}
}

View File

@@ -31,6 +31,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.integerArrayResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
@@ -65,6 +66,8 @@ import org.thoughtcrime.securesms.database.MediaTable
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
import org.thoughtcrime.securesms.preferences.widgets.StorageGraphView
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.viewModel
import java.text.NumberFormat
@@ -98,6 +101,7 @@ class ManageStorageSettingsFragment : ComposeFragment() {
onReviewStorage = { startActivity(MediaOverviewActivity.forAll(requireContext())) },
onSetKeepMessages = { navController.navigate("set-keep-messages") },
onSetChatLengthLimit = { navController.navigate("set-chat-length-limit") },
onSyncTrimThreadDeletes = { viewModel.setSyncTrimDeletes(it) },
onDeleteChatHistory = { navController.navigate("confirm-delete-chat-history") }
)
}
@@ -134,7 +138,11 @@ class ManageStorageSettingsFragment : ComposeFragment() {
dialog("confirm-delete-chat-history") {
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.preferences_storage__delete_message_history),
body = stringResource(id = R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device),
body = if (TextSecurePreferences.isMultiDevice(LocalContext.current) && FeatureFlags.deleteSyncEnabled()) {
stringResource(id = R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device_linked_device)
} else {
stringResource(id = R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device)
},
confirm = stringResource(id = R.string.delete),
confirmColor = MaterialTheme.colorScheme.error,
dismiss = stringResource(id = android.R.string.cancel),
@@ -146,7 +154,11 @@ class ManageStorageSettingsFragment : ComposeFragment() {
dialog("double-confirm-delete-chat-history", dialogProperties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) {
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.preferences_storage__are_you_sure_you_want_to_delete_all_message_history),
body = stringResource(id = R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone),
body = if (TextSecurePreferences.isMultiDevice(LocalContext.current) && FeatureFlags.deleteSyncEnabled()) {
stringResource(id = R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone_linked_device)
} else {
stringResource(id = R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone)
},
confirm = stringResource(id = R.string.preferences_storage__delete_all_now),
confirmColor = MaterialTheme.colorScheme.error,
dismiss = stringResource(id = android.R.string.cancel),
@@ -223,6 +235,7 @@ private fun ManageStorageSettingsScreen(
onReviewStorage: () -> Unit = {},
onSetKeepMessages: () -> Unit = {},
onSetChatLengthLimit: () -> Unit = {},
onSyncTrimThreadDeletes: (Boolean) -> Unit = {},
onDeleteChatHistory: () -> Unit = {}
) {
Scaffolds.Settings(
@@ -263,6 +276,13 @@ private fun ManageStorageSettingsScreen(
onClick = onSetChatLengthLimit
)
Rows.ToggleRow(
text = stringResource(id = R.string.ManageStorageSettingsFragment_apply_limits_title),
label = stringResource(id = R.string.ManageStorageSettingsFragment_apply_limits_description),
checked = state.syncTrimDeletes,
onCheckChanged = onSyncTrimThreadDeletes
)
Dividers.Default()
Rows.TextRow(

View File

@@ -26,7 +26,8 @@ class ManageStorageSettingsViewModel : ViewModel() {
private val store = MutableStateFlow(
ManageStorageState(
keepMessagesDuration = SignalStore.settings().keepMessagesDuration,
lengthLimit = if (SignalStore.settings().isTrimByLengthEnabled) SignalStore.settings().threadTrimLength else ManageStorageState.NO_LIMIT
lengthLimit = if (SignalStore.settings().isTrimByLengthEnabled) SignalStore.settings().threadTrimLength else ManageStorageState.NO_LIMIT,
syncTrimDeletes = SignalStore.settings().shouldSyncThreadTrimDeletes()
)
)
val state = store.asStateFlow()
@@ -82,6 +83,11 @@ class ManageStorageSettingsViewModel : ViewModel() {
return isRestrictingLengthLimitChange(newLimit)
}
fun setSyncTrimDeletes(syncTrimDeletes: Boolean) {
SignalStore.settings().setSyncThreadTrimDeletes(syncTrimDeletes)
store.update { it.copy(syncTrimDeletes = syncTrimDeletes) }
}
private fun isRestrictingLengthLimitChange(newLimit: Int): Boolean {
return state.value.lengthLimit == ManageStorageState.NO_LIMIT || (newLimit != ManageStorageState.NO_LIMIT && newLimit < state.value.lengthLimit)
}
@@ -90,6 +96,7 @@ class ManageStorageSettingsViewModel : ViewModel() {
data class ManageStorageState(
val keepMessagesDuration: KeepMessagesDuration = KeepMessagesDuration.FOREVER,
val lengthLimit: Int = NO_LIMIT,
val syncTrimDeletes: Boolean = true,
val breakdown: MediaTable.StorageBreakdown? = null
) {
companion object {