Convert and update Manage Storage Settings.

This commit is contained in:
Cody Henthorne
2024-04-30 11:32:27 -04:00
committed by Greyson Parrelli
parent adef572abb
commit 6d657b449c
17 changed files with 764 additions and 703 deletions

View File

@@ -20,9 +20,4 @@ public class BaseSettingsAdapter extends MappingAdapter {
registerFactory(SingleSelectSetting.Item.class,
new LayoutFactory<>(v -> new SingleSelectSetting.ViewHolder(v, selectionChangedListener), R.layout.single_select_item));
}
public void configureCustomizableSingleSelect(@NonNull CustomizableSingleSelectSetting.CustomizableSingleSelectionListener selectionListener) {
registerFactory(CustomizableSingleSelectSetting.Item.class,
new LayoutFactory<>(v -> new CustomizableSingleSelectSetting.ViewHolder(v, selectionListener), R.layout.customizable_single_select_item));
}
}

View File

@@ -1,93 +0,0 @@
package org.thoughtcrime.securesms.components.settings;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
import java.io.Serializable;
import java.util.Objects;
/**
* A simple settings screen that takes its configuration via {@link Configuration}.
*/
public class BaseSettingsFragment extends Fragment {
private static final String CONFIGURATION_ARGUMENT = "current_selection";
private RecyclerView recycler;
public static @NonNull BaseSettingsFragment create(@NonNull Configuration configuration) {
BaseSettingsFragment fragment = new BaseSettingsFragment();
Bundle arguments = new Bundle();
arguments.putSerializable(CONFIGURATION_ARGUMENT, configuration);
fragment.setArguments(arguments);
return fragment;
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.base_settings_fragment, container, false);
recycler = view.findViewById(R.id.base_settings_list);
recycler.setItemAnimator(null);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
BaseSettingsAdapter adapter = new BaseSettingsAdapter();
recycler.setLayoutManager(new LinearLayoutManager(requireContext()));
recycler.setAdapter(adapter);
Configuration configuration = (Configuration) Objects.requireNonNull(requireArguments().getSerializable(CONFIGURATION_ARGUMENT));
configuration.configure(requireActivity(), adapter);
configuration.setArguments(getArguments());
configuration.configureAdapter(adapter);
adapter.submitList(configuration.getSettings());
}
/**
* A configuration for a settings screen. Utilizes serializable to hide
* reflection of instantiating from a fragment argument.
*/
public static abstract class Configuration implements Serializable {
protected transient FragmentActivity activity;
protected transient BaseSettingsAdapter adapter;
public void configure(@NonNull FragmentActivity activity, @NonNull BaseSettingsAdapter adapter) {
this.activity = activity;
this.adapter = adapter;
}
/**
* Retrieve any runtime information from the fragment's arguments.
*/
public void setArguments(@Nullable Bundle arguments) {}
protected void updateSettingsList() {
adapter.submitList(getSettings());
}
public abstract void configureAdapter(@NonNull BaseSettingsAdapter adapter);
public abstract @NonNull MappingModelList getSettings();
}
}

View File

@@ -1,72 +0,0 @@
package org.thoughtcrime.securesms.components.settings;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.Group;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder;
import java.util.Objects;
/**
* Adds ability to customize a value for a single select (radio) setting.
*/
public class CustomizableSingleSelectSetting {
public interface CustomizableSingleSelectionListener extends SingleSelectSetting.SingleSelectSelectionChangedListener {
void onCustomizeClicked(@Nullable Item item);
}
public static class ViewHolder extends MappingViewHolder<Item> {
private final View customize;
private final SingleSelectSetting.ViewHolder delegate;
private final Group customizeGroup;
private final CustomizableSingleSelectionListener selectionListener;
public ViewHolder(@NonNull View itemView, @NonNull CustomizableSingleSelectionListener selectionListener) {
super(itemView);
this.selectionListener = selectionListener;
customize = findViewById(R.id.customizable_single_select_customize);
customizeGroup = findViewById(R.id.customizable_single_select_customize_group);
delegate = new SingleSelectSetting.ViewHolder(itemView, selectionListener);
}
@Override
public void bind(@NonNull Item model) {
delegate.bind(model.singleSelectItem);
customizeGroup.setVisibility(model.singleSelectItem.isSelected() ? View.VISIBLE : View.GONE);
customize.setOnClickListener(v -> selectionListener.onCustomizeClicked(model));
}
}
public static class Item implements MappingModel<Item> {
private final SingleSelectSetting.Item singleSelectItem;
private final Object customValue;
public <T> Item(@NonNull T item, @Nullable String text, boolean isSelected, @Nullable Object customValue, @Nullable String summaryText) {
this.customValue = customValue;
singleSelectItem = new SingleSelectSetting.Item(item, text, summaryText, isSelected);
}
public @Nullable Object getCustomValue() {
return customValue;
}
@Override
public boolean areItemsTheSame(@NonNull Item newItem) {
return singleSelectItem.areItemsTheSame(newItem.singleSelectItem);
}
@Override
public boolean areContentsTheSame(@NonNull Item newItem) {
return singleSelectItem.areContentsTheSame(newItem.singleSelectItem) && Objects.equals(customValue, newItem.customValue);
}
}
}

View File

@@ -0,0 +1,523 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.storage
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.res.integerArrayResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.dialog
import androidx.navigation.compose.rememberNavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.navArgument
import org.signal.core.ui.Animations
import org.signal.core.ui.Dialogs
import org.signal.core.ui.Dividers
import org.signal.core.ui.Previews
import org.signal.core.ui.Rows
import org.signal.core.ui.Rows.TextAndLabel
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
import org.signal.core.ui.Texts
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
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.Util
import org.thoughtcrime.securesms.util.viewModel
import java.text.NumberFormat
/**
* Manage settings related to on-device storage including viewing usage and auto-delete settings.
*/
class ManageStorageSettingsFragment : ComposeFragment() {
private val viewModel by viewModel<ManageStorageSettingsViewModel> { ManageStorageSettingsViewModel() }
@Composable
override fun FragmentContent() {
val state by viewModel.state.collectAsState()
val navController = rememberNavController()
SignalTheme {
NavHost(
navController = navController,
startDestination = "manage",
enterTransition = { Animations.navHostSlideInTransition { it } },
exitTransition = { Animations.navHostSlideOutTransition { -it } },
popEnterTransition = { Animations.navHostSlideInTransition { -it } },
popExitTransition = { Animations.navHostSlideOutTransition { it } }
) {
composable("manage") {
ManageStorageSettingsScreen(
state = state,
onNavigationClick = { findNavController().popBackStack() },
onReviewStorage = { startActivity(MediaOverviewActivity.forAll(requireContext())) },
onSetKeepMessages = { navController.navigate("set-keep-messages") },
onSetChatLengthLimit = { navController.navigate("set-chat-length-limit") },
onDeleteChatHistory = { navController.navigate("confirm-delete-chat-history") }
)
}
composable("set-keep-messages") {
SetKeepMessagesScreen(
selection = state.keepMessagesDuration,
onNavigationClick = { navController.popBackStack() },
onSelectionChanged = { newDuration ->
if (viewModel.showConfirmKeepDurationChange(newDuration)) {
navController.navigate("confirm-set-keep-messages/${newDuration.id}")
} else {
viewModel.setKeepMessagesDuration(newDuration)
}
}
)
}
composable("set-chat-length-limit") {
SetChatLengthLimitScreen(
currentLimit = state.lengthLimit,
onNavigationClick = { navController.popBackStack() },
onOptionSelected = { newLengthLimit ->
if (viewModel.showConfirmSetChatLengthLimit(newLengthLimit)) {
navController.navigate("confirm-set-length-limit/$newLengthLimit")
} else {
viewModel.setChatLengthLimit(newLengthLimit)
}
},
onCustomSelected = { navController.navigate("custom-set-length-limit") }
)
}
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),
confirm = stringResource(id = R.string.delete),
confirmColor = MaterialTheme.colorScheme.error,
dismiss = stringResource(id = android.R.string.cancel),
onConfirm = { navController.navigate("double-confirm-delete-chat-history") },
onDismiss = { navController.popBackStack() }
)
}
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),
confirm = stringResource(id = R.string.preferences_storage__delete_all_now),
confirmColor = MaterialTheme.colorScheme.error,
dismiss = stringResource(id = android.R.string.cancel),
onConfirm = { viewModel.deleteChatHistory() },
onDismiss = { navController.popBackStack() }
)
}
dialog(
route = "confirm-set-keep-messages/{keepMessagesId}",
arguments = listOf(navArgument("keepMessagesId") { type = NavType.IntType })
) {
val newDuration = KeepMessagesDuration.fromId(it.arguments!!.getInt("keepMessagesId"))
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.preferences_storage__delete_older_messages),
body = stringResource(id = R.string.preferences_storage__this_will_permanently_delete_all_message_history_and_media, stringResource(id = newDuration.stringResource)),
confirm = stringResource(id = R.string.delete),
dismiss = stringResource(id = android.R.string.cancel),
onConfirm = { viewModel.setKeepMessagesDuration(newDuration) },
onDismiss = { navController.popBackStack() }
)
}
dialog(
route = "confirm-set-length-limit/{lengthLimit}",
arguments = listOf(navArgument("lengthLimit") { type = NavType.IntType })
) {
val newLengthLimit = it.arguments!!.getInt("lengthLimit")
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.preferences_storage__delete_older_messages),
body = pluralStringResource(
id = R.plurals.preferences_storage__this_will_permanently_trim_all_conversations_to_the_d_most_recent_messages,
count = newLengthLimit,
NumberFormat.getInstance().format(newLengthLimit)
),
confirm = stringResource(id = R.string.delete),
dismiss = stringResource(id = android.R.string.cancel),
onConfirm = { viewModel.setChatLengthLimit(newLengthLimit) },
onDismiss = { navController.popBackStack() }
)
}
dialog(
route = "custom-set-length-limit"
) {
SetCustomLengthLimitDialog(
currentLimit = if (state.lengthLimit != ManageStorageSettingsViewModel.ManageStorageState.NO_LIMIT) state.lengthLimit else null,
onCustomLimitSet = { newLengthLimit ->
if (viewModel.showConfirmSetChatLengthLimit(newLengthLimit)) {
navController.navigate("confirm-set-length-limit/$newLengthLimit")
} else {
viewModel.setChatLengthLimit(newLengthLimit)
}
},
onDismiss = { navController.popBackStack() }
)
}
}
}
}
override fun onResume() {
super.onResume()
viewModel.refresh()
}
}
@Composable
private fun ManageStorageSettingsScreen(
state: ManageStorageSettingsViewModel.ManageStorageState,
onNavigationClick: () -> Unit = {},
onReviewStorage: () -> Unit = {},
onSetKeepMessages: () -> Unit = {},
onSetChatLengthLimit: () -> Unit = {},
onDeleteChatHistory: () -> Unit = {}
) {
Scaffolds.Settings(
title = stringResource(id = R.string.preferences__storage),
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24)
) { contentPadding ->
Column(
modifier = Modifier
.padding(contentPadding)
.verticalScroll(rememberScrollState())
) {
Texts.SectionHeader(text = stringResource(id = R.string.preferences_storage__storage_usage))
StorageOverview(state.breakdown, onReviewStorage)
Dividers.Default()
Texts.SectionHeader(text = stringResource(id = R.string.ManageStorageSettingsFragment_chat_limit))
Rows.TextRow(
text = stringResource(id = R.string.preferences__keep_messages),
label = stringResource(id = state.keepMessagesDuration.stringResource),
onClick = onSetKeepMessages
)
Rows.TextRow(
text = stringResource(id = R.string.preferences__conversation_length_limit),
label = if (state.lengthLimit != ManageStorageSettingsViewModel.ManageStorageState.NO_LIMIT) {
pluralStringResource(
id = R.plurals.preferences_storage__s_messages_plural,
count = state.lengthLimit,
NumberFormat.getInstance().format(state.lengthLimit)
)
} else {
stringResource(id = R.string.preferences_storage__none)
},
onClick = onSetChatLengthLimit
)
Dividers.Default()
Rows.TextRow(
text = stringResource(id = R.string.ManageStorageSettingsFragment_delete_message_history),
onClick = onDeleteChatHistory
)
}
}
}
@Composable
private fun StorageOverview(
breakdown: MediaTable.StorageBreakdown?,
onReviewStorage: () -> Unit
) {
AndroidView(
factory = {
LayoutInflater.from(it).inflate(R.layout.preference_storage_category, null)
},
modifier = Modifier.fillMaxWidth()
) {
if (breakdown != null) {
val breakdownEntries = StorageGraphView.StorageBreakdown(
listOf(
StorageGraphView.Entry(ContextCompat.getColor(it.context, R.color.storage_color_photos), breakdown.photoSize),
StorageGraphView.Entry(ContextCompat.getColor(it.context, R.color.storage_color_videos), breakdown.videoSize),
StorageGraphView.Entry(ContextCompat.getColor(it.context, R.color.storage_color_files), breakdown.documentSize),
StorageGraphView.Entry(ContextCompat.getColor(it.context, R.color.storage_color_audio), breakdown.audioSize)
)
)
it.findViewById<StorageGraphView>(R.id.storageGraphView).setStorageBreakdown(breakdownEntries)
it.findViewById<TextView>(R.id.total_size).text = Util.getPrettyFileSize(breakdownEntries.totalSize)
}
it.findViewById<View>(R.id.free_up_space).setOnClickListener {
onReviewStorage()
}
}
}
@Composable
private fun SetKeepMessagesScreen(
selection: KeepMessagesDuration,
onNavigationClick: () -> Unit = {},
onSelectionChanged: (KeepMessagesDuration) -> Unit = {}
) {
Scaffolds.Settings(
title = stringResource(id = R.string.preferences__keep_messages),
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24)
) { contentPadding ->
Column(
modifier = Modifier
.padding(contentPadding)
.verticalScroll(rememberScrollState())
) {
KeepMessagesDuration
.values()
.forEach {
Rows.RadioRow(
text = stringResource(id = it.stringResource),
selected = it == selection,
modifier = Modifier.clickable { onSelectionChanged(it) }
)
}
Rows.TextRow(
text = {
Text(
text = stringResource(id = R.string.ManageStorageSettingsFragment_keep_messages_duration_warning),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
}
}
}
@Composable
private fun SetChatLengthLimitScreen(
currentLimit: Int,
onNavigationClick: () -> Unit = {},
onOptionSelected: (Int) -> Unit = {},
onCustomSelected: (Int) -> Unit = {}
) {
Scaffolds.Settings(
title = stringResource(id = R.string.preferences__conversation_length_limit),
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24)
) { contentPadding ->
Column(
modifier = Modifier
.padding(contentPadding)
.verticalScroll(rememberScrollState())
) {
val options = integerArrayResource(id = R.array.conversation_length_limit)
var customSelected = true
for (option in options) {
val isSelected = option == currentLimit
Rows.RadioRow(
selected = isSelected,
text = if (option == 0) {
stringResource(id = R.string.preferences_storage__none)
} else {
pluralStringResource(id = R.plurals.preferences_storage__s_messages_plural, count = option, NumberFormat.getInstance().format(option.toLong()))
},
modifier = Modifier.clickable { onOptionSelected(option) }
)
if (isSelected) {
customSelected = false
}
}
Rows.RadioRow(
content = {
TextAndLabel(
text = stringResource(id = R.string.preferences_storage__custom),
label = if (customSelected) {
pluralStringResource(id = R.plurals.preferences_storage__s_messages_plural, count = currentLimit, NumberFormat.getInstance().format(currentLimit))
} else {
null
}
)
if (customSelected) {
Dividers.Vertical(
modifier = Modifier
.padding(horizontal = 8.dp)
.height(48.dp)
)
Icon(
painter = painterResource(id = R.drawable.symbol_settings_android_24),
contentDescription = null,
modifier = Modifier
.clickable { onCustomSelected(currentLimit) }
.padding(12.dp)
)
}
},
selected = customSelected,
modifier = Modifier.clickable { onCustomSelected(currentLimit) }
)
Rows.TextRow(
text = {
Text(
text = stringResource(id = R.string.ManageStorageSettingsFragment_chat_length_limit_warning),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
}
}
}
@Composable
private fun SetCustomLengthLimitDialog(
currentLimit: Int? = 1000,
onCustomLimitSet: (Int) -> Unit = {},
onDismiss: () -> Unit = {}
) {
var lengthLimit by remember {
mutableStateOf(
TextFieldValue(
text = currentLimit?.toString() ?: "",
selection = TextRange(currentLimit.toString().length)
)
)
}
val focusRequester = remember { FocusRequester() }
androidx.compose.material3.AlertDialog(
onDismissRequest = onDismiss,
title = { Text(text = stringResource(id = R.string.preferences__conversation_length_limit)) },
text = {
TextField(
value = lengthLimit,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = { value ->
val cleaned = value.text.replace("\\D".toRegex(), "")
lengthLimit = if (cleaned == value.text) {
value
} else {
value.copy(text = cleaned)
}
},
modifier = Modifier.focusRequester(focusRequester)
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
},
confirmButton = {
TextButton(
enabled = lengthLimit.text.toIntOrNull() != null,
onClick = {
onDismiss()
onCustomLimitSet(lengthLimit.text.toInt())
}
) {
Text(text = stringResource(id = android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = stringResource(id = android.R.string.cancel))
}
}
)
}
@SignalPreview
@Composable
private fun ManageStorageSettingsScreenPreview() {
Previews.Preview {
ManageStorageSettingsScreen(
state = ManageStorageSettingsViewModel.ManageStorageState(
keepMessagesDuration = KeepMessagesDuration.FOREVER,
lengthLimit = ManageStorageSettingsViewModel.ManageStorageState.NO_LIMIT
)
)
}
}
@SignalPreview
@Composable
private fun SetKeepMessagesScreenPreview() {
Previews.Preview {
SetKeepMessagesScreen(selection = KeepMessagesDuration.FOREVER)
}
}
@SignalPreview
@Composable
private fun SetChatLengthLimitScreenPreview() {
Previews.Preview {
SetChatLengthLimitScreen(
currentLimit = 1000
)
}
}
@SignalPreview
@Composable
private fun SetCustomLengthLimitDialogPreview() {
Previews.Preview {
SetCustomLengthLimitDialog(currentLimit = 123)
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.storage
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
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.database.MediaTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.media
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration
import org.thoughtcrime.securesms.keyvalue.SignalStore
class ManageStorageSettingsViewModel : ViewModel() {
private val store = MutableStateFlow(
ManageStorageState(
keepMessagesDuration = SignalStore.settings().keepMessagesDuration,
lengthLimit = if (SignalStore.settings().isTrimByLengthEnabled) SignalStore.settings().threadTrimLength else ManageStorageState.NO_LIMIT
)
)
val state = store.asStateFlow()
fun refresh() {
viewModelScope.launch {
val breakdown: MediaTable.StorageBreakdown = media.getStorageBreakdown()
store.update { it.copy(breakdown = breakdown) }
}
}
fun deleteChatHistory() {
viewModelScope.launch {
SignalDatabase.threads.deleteAllConversations()
ApplicationDependencies.getMessageNotifier().updateNotification(ApplicationDependencies.getApplication())
}
}
fun setKeepMessagesDuration(newDuration: KeepMessagesDuration) {
SignalStore.settings().setKeepMessagesForDuration(newDuration)
ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary()
store.update { it.copy(keepMessagesDuration = newDuration) }
}
fun showConfirmKeepDurationChange(newDuration: KeepMessagesDuration): Boolean {
return newDuration.ordinal > state.value.keepMessagesDuration.ordinal
}
fun setChatLengthLimit(newLimit: Int) {
val restrictingChange = isRestrictingLengthLimitChange(newLimit)
SignalStore.settings().setThreadTrimByLengthEnabled(newLimit != ManageStorageState.NO_LIMIT)
SignalStore.settings().threadTrimLength = newLimit
store.update { it.copy(lengthLimit = newLimit) }
if (SignalStore.settings().isTrimByLengthEnabled && restrictingChange) {
SignalExecutors.BOUNDED.execute {
val keepMessagesDuration = SignalStore.settings().keepMessagesDuration
val trimBeforeDate = if (keepMessagesDuration != KeepMessagesDuration.FOREVER) {
System.currentTimeMillis() - keepMessagesDuration.duration
} else {
ThreadTable.NO_TRIM_BEFORE_DATE_SET
}
SignalDatabase.threads.trimAllThreads(newLimit, trimBeforeDate)
}
}
}
fun showConfirmSetChatLengthLimit(newLimit: Int): Boolean {
return isRestrictingLengthLimitChange(newLimit)
}
private fun isRestrictingLengthLimitChange(newLimit: Int): Boolean {
return state.value.lengthLimit == ManageStorageState.NO_LIMIT || (newLimit != ManageStorageState.NO_LIMIT && newLimit < state.value.lengthLimit)
}
@Immutable
data class ManageStorageState(
val keepMessagesDuration: KeepMessagesDuration = KeepMessagesDuration.FOREVER,
val lengthLimit: Int = NO_LIMIT,
val breakdown: MediaTable.StorageBreakdown? = null
) {
companion object {
const val NO_LIMIT = 0
}
}
}

View File

@@ -1,10 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.wrapped
import androidx.fragment.app.Fragment
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment
class WrappedStoragePreferenceFragment : SettingsWrapperFragment() {
override fun getFragment(): Fragment {
return StoragePreferenceFragment()
}
}

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database
import android.annotation.SuppressLint
import android.content.Context
import android.database.Cursor
import androidx.compose.runtime.Immutable
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
@@ -302,6 +303,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
}
}
@Immutable
data class StorageBreakdown(
val photoSize: Long,
val videoSize: Long,

View File

@@ -35,7 +35,7 @@ public enum KeepMessagesDuration {
return duration;
}
static @NonNull KeepMessagesDuration fromId(int id) {
public static @NonNull KeepMessagesDuration fromId(int id) {
return values()[id];
}
}

View File

@@ -1,47 +0,0 @@
package org.thoughtcrime.securesms.preferences;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MediaTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.preferences.widgets.StorageGraphView;
import java.util.Arrays;
public class ApplicationPreferencesViewModel extends ViewModel {
private final MutableLiveData<StorageGraphView.StorageBreakdown> storageBreakdown = new MutableLiveData<>();
LiveData<StorageGraphView.StorageBreakdown> getStorageBreakdown() {
return storageBreakdown;
}
static ApplicationPreferencesViewModel getApplicationPreferencesViewModel(@NonNull FragmentActivity activity) {
return new ViewModelProvider(activity).get(ApplicationPreferencesViewModel.class);
}
void refreshStorageBreakdown(@NonNull Context context) {
SignalExecutors.BOUNDED.execute(() -> {
MediaTable.StorageBreakdown breakdown = SignalDatabase.media().getStorageBreakdown();
StorageGraphView.StorageBreakdown latestStorageBreakdown = new StorageGraphView.StorageBreakdown(Arrays.asList(
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_photos), breakdown.getPhotoSize()),
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_videos), breakdown.getVideoSize()),
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_files), breakdown.getDocumentSize()),
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_audio), breakdown.getAudioSize())
));
storageBreakdown.postValue(latestStorageBreakdown);
});
}
}

View File

@@ -1,312 +0,0 @@
package org.thoughtcrime.securesms.preferences;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.Preference;
import com.annimon.stream.Stream;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.settings.BaseSettingsAdapter;
import org.thoughtcrime.securesms.components.settings.BaseSettingsFragment;
import org.thoughtcrime.securesms.components.settings.CustomizableSingleSelectSetting;
import org.thoughtcrime.securesms.components.settings.SingleSelectSetting;
import org.thoughtcrime.securesms.components.settings.app.wrapped.SettingsWrapperFragment;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadTable;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
import org.thoughtcrime.securesms.keyvalue.SettingsValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.preferences.widgets.StoragePreferenceCategory;
import org.signal.core.util.StringUtil;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
import java.text.NumberFormat;
public class StoragePreferenceFragment extends ListSummaryPreferenceFragment {
private Preference keepMessages;
private Preference trimLength;
@Override
public void onCreate(@Nullable Bundle paramBundle) {
super.onCreate(paramBundle);
findPreference("pref_storage_clear_message_history")
.setOnPreferenceClickListener(new ClearMessageHistoryClickListener());
trimLength = findPreference(SettingsValues.THREAD_TRIM_LENGTH);
trimLength.setOnPreferenceClickListener(p -> {
updateToolbarTitle(R.string.preferences__conversation_length_limit);
pushFragment(BaseSettingsFragment.create(new ConversationLengthLimitConfiguration()));
return true;
});
keepMessages = findPreference(SettingsValues.KEEP_MESSAGES_DURATION);
keepMessages.setOnPreferenceClickListener(p -> {
updateToolbarTitle(R.string.preferences__keep_messages);
pushFragment(BaseSettingsFragment.create(new KeepMessagesConfiguration()));
return true;
});
StoragePreferenceCategory storageCategory = (StoragePreferenceCategory) findPreference("pref_storage_category");
FragmentActivity activity = requireActivity();
ApplicationPreferencesViewModel viewModel = ApplicationPreferencesViewModel.getApplicationPreferencesViewModel(activity);
storageCategory.setOnFreeUpSpace(() -> activity.startActivity(MediaOverviewActivity.forAll(activity)));
viewModel.getStorageBreakdown().observe(activity, storageCategory::setStorage);
}
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
addPreferencesFromResource(R.xml.preferences_storage);
}
@Override
public void onResume() {
super.onResume();
updateToolbarTitle(R.string.preferences__storage);
FragmentActivity activity = requireActivity();
ApplicationPreferencesViewModel viewModel = ApplicationPreferencesViewModel.getApplicationPreferencesViewModel(activity);
viewModel.refreshStorageBreakdown(activity.getApplicationContext());
keepMessages.setSummary(SignalStore.settings().getKeepMessagesDuration().getStringResource());
trimLength.setSummary(SignalStore.settings().isTrimByLengthEnabled() ? getResources().getQuantityString(R.plurals.preferences_storage__s_messages_plural, SignalStore.settings().getThreadTrimLength(), NumberFormat.getInstance().format(SignalStore.settings().getThreadTrimLength()))
: getString(R.string.preferences_storage__none));
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
private void updateToolbarTitle(@StringRes int title) {
if (getParentFragment() instanceof SettingsWrapperFragment) {
((SettingsWrapperFragment) getParentFragment()).setTitle(title);
}
}
private void pushFragment(@NonNull Fragment fragment) {
getParentFragmentManager().beginTransaction()
.replace(R.id.wrapped_fragment, fragment)
.addToBackStack(null)
.commit();
}
private class ClearMessageHistoryClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
new MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.preferences_storage__clear_message_history)
.setMessage(R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device)
.setPositiveButton(R.string.delete, (d, w) -> showAreYouReallySure())
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;
}
private void showAreYouReallySure() {
new MaterialAlertDialogBuilder(requireActivity())
.setTitle(R.string.preferences_storage__are_you_sure_you_want_to_delete_all_message_history)
.setMessage(R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone)
.setPositiveButton(R.string.preferences_storage__delete_all_now, (d, w) -> {
SignalExecutors.BOUNDED.execute(() -> {
SignalDatabase.threads().deleteAllConversations();
ApplicationDependencies.getMessageNotifier().updateNotification(requireContext());
});
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
}
public static class KeepMessagesConfiguration extends BaseSettingsFragment.Configuration implements SingleSelectSetting.SingleSelectSelectionChangedListener {
@Override
public void configureAdapter(@NonNull BaseSettingsAdapter adapter) {
adapter.configureSingleSelect(this);
}
@Override
public @NonNull MappingModelList getSettings() {
KeepMessagesDuration currentDuration = SignalStore.settings().getKeepMessagesDuration();
return Stream.of(KeepMessagesDuration.values())
.map(duration -> new SingleSelectSetting.Item(duration, activity.getString(duration.getStringResource()), null, duration.equals(currentDuration)))
.collect(MappingModelList.toMappingModelList());
}
@Override
public void onSelectionChanged(@NonNull Object selection) {
KeepMessagesDuration currentDuration = SignalStore.settings().getKeepMessagesDuration();
KeepMessagesDuration newDuration = (KeepMessagesDuration) selection;
if (newDuration.ordinal() > currentDuration.ordinal()) {
new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.preferences_storage__delete_older_messages)
.setMessage(activity.getString(R.string.preferences_storage__this_will_permanently_delete_all_message_history_and_media, activity.getString(newDuration.getStringResource())))
.setPositiveButton(R.string.delete, (d, w) -> updateTrimByTime(newDuration))
.setNegativeButton(android.R.string.cancel, null)
.show();
} else {
updateTrimByTime(newDuration);
}
}
private void updateTrimByTime(@NonNull KeepMessagesDuration newDuration) {
SignalStore.settings().setKeepMessagesForDuration(newDuration);
updateSettingsList();
ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary();
}
}
public static class ConversationLengthLimitConfiguration extends BaseSettingsFragment.Configuration implements CustomizableSingleSelectSetting.CustomizableSingleSelectionListener {
private static final int CUSTOM_LENGTH = -1;
@Override
public void configureAdapter(@NonNull BaseSettingsAdapter adapter) {
adapter.configureSingleSelect(this);
adapter.configureCustomizableSingleSelect(this);
}
@Override
public @NonNull MappingModelList getSettings() {
int trimLength = SignalStore.settings().isTrimByLengthEnabled() ? SignalStore.settings().getThreadTrimLength() : 0;
int[] options = activity.getResources().getIntArray(R.array.conversation_length_limit);
boolean hasSelection = false;
MappingModelList settings = new MappingModelList();
for (int option : options) {
boolean isSelected = option == trimLength;
String text = option == 0 ? activity.getString(R.string.preferences_storage__none)
: activity.getResources().getQuantityString(R.plurals.preferences_storage__s_messages_plural, option, NumberFormat.getInstance().format(option));
settings.add(new SingleSelectSetting.Item(option, text, null, isSelected));
hasSelection = hasSelection || isSelected;
}
int currentValue = SignalStore.settings().getThreadTrimLength();
settings.add(new CustomizableSingleSelectSetting.Item(CUSTOM_LENGTH,
activity.getString(R.string.preferences_storage__custom),
!hasSelection,
currentValue,
activity.getResources().getQuantityString(R.plurals.preferences_storage__s_messages_plural, currentValue, NumberFormat.getInstance().format(currentValue))));
return settings;
}
@SuppressLint("InflateParams")
@Override
public void onCustomizeClicked(@Nullable CustomizableSingleSelectSetting.Item item) {
boolean trimLengthEnabled = SignalStore.settings().isTrimByLengthEnabled();
int trimLength = trimLengthEnabled ? SignalStore.settings().getThreadTrimLength() : 0;
View view = LayoutInflater.from(activity).inflate(R.layout.customizable_setting_edit_text, null, false);
EditText editText = view.findViewById(R.id.customizable_setting_edit_text);
if (trimLength > 0) {
editText.setText(String.valueOf(trimLength));
}
AlertDialog dialog = new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.preferences__conversation_length_limit)
.setView(view)
.setPositiveButton(android.R.string.ok, (d, w) -> onSelectionChanged(Integer.parseInt(editText.getText().toString())))
.setNegativeButton(android.R.string.cancel, (d, w) -> updateSettingsList())
.create();
dialog.setOnShowListener(d -> {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(editText.getText()));
editText.requestFocus();
editText.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(@NonNull Editable sequence) {
CharSequence trimmed = StringUtil.trimSequence(sequence);
if (TextUtils.isEmpty(trimmed)) {
sequence.replace(0, sequence.length(), "");
} else {
try {
Integer.parseInt(trimmed.toString());
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
return;
} catch (NumberFormatException e) {
String onlyDigits = trimmed.toString().replaceAll("[^\\d]", "");
if (!onlyDigits.equals(trimmed.toString())) {
sequence.replace(0, sequence.length(), onlyDigits);
}
}
}
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
@Override
public void beforeTextChanged(@NonNull CharSequence sequence, int start, int count, int after) {}
@Override
public void onTextChanged(@NonNull CharSequence sequence, int start, int before, int count) {}
});
});
dialog.show();
}
@Override
public void onSelectionChanged(@NonNull Object selection) {
boolean trimLengthEnabled = SignalStore.settings().isTrimByLengthEnabled();
int trimLength = trimLengthEnabled ? SignalStore.settings().getThreadTrimLength() : 0;
int newTrimLength = (Integer) selection;
if (newTrimLength > 0 && (!trimLengthEnabled || newTrimLength < trimLength)) {
new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.preferences_storage__delete_older_messages)
.setMessage(activity.getResources().getQuantityString(R.plurals.preferences_storage__this_will_permanently_trim_all_conversations_to_the_d_most_recent_messages, newTrimLength, newTrimLength))
.setPositiveButton(R.string.delete, (d, w) -> updateTrimByLength(newTrimLength))
.setNegativeButton(android.R.string.cancel, null)
.show();
} else if (newTrimLength == CUSTOM_LENGTH) {
onCustomizeClicked(null);
} else {
updateTrimByLength(newTrimLength);
}
}
private void updateTrimByLength(int length) {
boolean restrictingChange = !SignalStore.settings().isTrimByLengthEnabled() || length < SignalStore.settings().getThreadTrimLength();
SignalStore.settings().setThreadTrimByLengthEnabled(length > 0);
SignalStore.settings().setThreadTrimLength(length);
updateSettingsList();
if (SignalStore.settings().isTrimByLengthEnabled() && restrictingChange) {
KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration();
long trimBeforeDate = keepMessagesDuration != KeepMessagesDuration.FOREVER ? System.currentTimeMillis() - keepMessagesDuration.getDuration()
: ThreadTable.NO_TRIM_BEFORE_DATE_SET;
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.threads().trimAllThreads(length, trimBeforeDate));
}
}
}
}

View File

@@ -1,73 +0,0 @@
package org.thoughtcrime.securesms.preferences.widgets;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.TextView;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceViewHolder;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.Util;
public final class StoragePreferenceCategory extends PreferenceCategory {
private Runnable onFreeUpSpace;
private TextView totalSize;
private StorageGraphView storageGraphView;
private StorageGraphView.StorageBreakdown storage;
public StoragePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
public StoragePreferenceCategory(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public StoragePreferenceCategory(Context context) {
super(context);
initialize();
}
private void initialize() {
setLayoutResource(R.layout.preference_storage_category);
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
totalSize = (TextView) view.findViewById(R.id.total_size);
storageGraphView = (StorageGraphView) view.findViewById(R.id.storageGraphView);
view.findViewById(R.id.free_up_space)
.setOnClickListener(v -> {
if (onFreeUpSpace != null) {
onFreeUpSpace.run();
}
});
totalSize.setText(Util.getPrettyFileSize(0));
if (storage != null) {
setStorage(storage);
}
}
public void setOnFreeUpSpace(Runnable onFreeUpSpace) {
this.onFreeUpSpace = onFreeUpSpace;
}
public void setStorage(StorageGraphView.StorageBreakdown storage) {
this.storage = storage;
if (totalSize != null) {
totalSize.setText(Util.getPrettyFileSize(storage.getTotalSize()));
}
if (storageGraphView != null) {
storageGraphView.setStorageBreakdown(storage);
}
}
}

View File

@@ -80,21 +80,15 @@
app:layout_constraintStart_toEndOf="@+id/legend_audio"
app:layout_constraintTop_toTopOf="@+id/legend_audio" />
<TextView
<com.google.android.material.button.MaterialButton
android:id="@+id/free_up_space"
android:layout_width="match_parent"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:gravity="center"
android:layout_marginTop="16dp"
android:text="@string/preferences_storage__review_storage"
android:textAllCaps="true"
android:textColor="@color/core_ultramarine"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/legend_photos" />

View File

@@ -367,7 +367,7 @@
<fragment
android:id="@+id/storagePreferenceFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.wrapped.WrappedStoragePreferenceFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.storage.ManageStorageSettingsFragment"
android:label="storage_preference_fragment" />
<fragment

View File

@@ -3279,7 +3279,8 @@
<string name="preferences_storage__audio">Audio</string>
<string name="preferences_storage__review_storage">Review storage</string>
<string name="preferences_storage__delete_older_messages">Delete older messages?</string>
<string name="preferences_storage__clear_message_history">Clear message history?</string>
<!-- Dialog title shown when selecting delete message history from manage storage -->
<string name="preferences_storage__delete_message_history">Delete message history?</string>
<string name="preferences_storage__this_will_permanently_delete_all_message_history_and_media">This will permanently delete all message history and media from your device that are older than %1$s.</string>
<!-- The body of an alert dialog that is shown when confirming a trim operation. Trimming will delete all but the most recent messages in a chat. The placeholder represents how many messages are kept in each chat. All older messages are deleted. -->
<plurals name="preferences_storage__this_will_permanently_trim_all_conversations_to_the_d_most_recent_messages">
@@ -6825,6 +6826,15 @@
<!-- Displayed as the user\'s payment method as a label in a preference row -->
<string name="BackupsTypeSettingsFragment__unknown">Unknown</string>
<!-- ManageStorageSettingsFragment -->
<!-- Settings section title header show above manage settings options for automatic chat history management (deleting) -->
<string name="ManageStorageSettingsFragment_chat_limit">Chat limits</string>
<!-- Settings row title text that can be clicked to delete all messages and associated data from the device -->
<string name="ManageStorageSettingsFragment_delete_message_history">Delete message history</string>
<!-- Warning message at the bottom of a settings screen indicating how messages will be deleted based on user's selection (time is a range like 30 days or 1 year etc) -->
<string name="ManageStorageSettingsFragment_keep_messages_duration_warning">Messages older than the selected time will be permanently deleted.</string>
<!-- Warning message at the bottom of a settings screen indicating how messages will be deleted based on user's selection (limit is a number like 500 or 5,000) -->
<string name="ManageStorageSettingsFragment_chat_length_limit_warning">Messages exceeding the selected length will be permanently deleted.</string>
<!-- EOF -->
</resources>

View File

@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<PreferenceCategory android:title="@string/preferences_storage__storage_usage" />
<org.thoughtcrime.securesms.preferences.widgets.StoragePreferenceCategory android:key="pref_storage_category" />
<PreferenceCategory android:layout="@layout/preference_divider" />
<PreferenceCategory
android:key="storage_limits"
android:title="@string/preferences_chats__message_history">
<Preference
android:key="settings.keep_messages_duration"
android:title="@string/preferences__keep_messages"
tools:summary="@string/preferences_storage__forever" />
<Preference
android:inputType="number"
android:key="pref_trim_length"
android:title="@string/preferences__conversation_length_limit"
tools:summary="None" />
<Preference
android:key="pref_storage_clear_message_history"
android:title="@string/preferences__clear_message_history" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -86,8 +86,8 @@ object Dialogs {
text = { Text(text = body) },
confirmButton = {
TextButton(onClick = {
onConfirm()
onDismiss()
onConfirm()
}) {
Text(text = confirm, color = confirmColor)
}

View File

@@ -26,16 +26,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.ui.Rows.TextAndLabel
object Rows {
/**
* A row consisting of a radio button and text, which takes up the full
* width of the screen.
* A row consisting of a radio button and [text] and optional [label] in a [TextAndLabel].
*/
@Composable
fun RadioRow(
@@ -44,6 +42,30 @@ object Rows {
modifier: Modifier = Modifier,
label: String? = null,
enabled: Boolean = true
) {
RadioRow(
content = {
TextAndLabel(
text = text,
label = label,
enabled = enabled
)
},
selected = selected,
modifier = modifier,
enabled = enabled
)
}
/**
* Customizable radio row that allows [content] to be provided as composable functions instead of primitives.
*/
@Composable
fun RadioRow(
content: @Composable RowScope.() -> Unit,
selected: Boolean,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
Row(
modifier = modifier
@@ -58,70 +80,66 @@ object Rows {
modifier = Modifier.padding(end = 24.dp)
)
Column(
modifier = Modifier.alpha(if (enabled) 1f else 0.4f)
) {
Text(
text = text,
style = MaterialTheme.typography.bodyLarge
)
if (label != null) {
Text(
text = label,
fontSize = 14.sp,
lineHeight = 20.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
content()
}
}
/**
* Row that positions [text] and optional [label] in a [TextAndLabel] to the side of a [Switch].
*/
@Composable
fun ToggleRow(
checked: Boolean,
text: String,
textColor: Color = MaterialTheme.colorScheme.onSurface,
onCheckChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
label: String? = null,
textColor: Color = MaterialTheme.colorScheme.onSurface,
enabled: Boolean = true
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(defaultPadding())
.padding(defaultPadding()),
verticalAlignment = CenterVertically
) {
Text(
TextAndLabel(
text = text,
color = textColor,
modifier = Modifier
.weight(1f)
.align(CenterVertically)
label = label,
textColor = textColor,
enabled = enabled,
modifier = Modifier.padding(end = 16.dp)
)
Switch(
checked = checked,
onCheckedChange = onCheckChanged,
modifier = Modifier.align(CenterVertically)
enabled = enabled,
onCheckedChange = onCheckChanged
)
}
}
/**
* Text row that positions [text] and optional [label] in a [TextAndLabel] to the side of an optional [icon].
*/
@Composable
fun TextRow(
text: String,
modifier: Modifier = Modifier,
iconModifier: Modifier = Modifier,
label: String? = null,
icon: Painter? = null,
foregroundTint: Color = MaterialTheme.colorScheme.onSurface,
onClick: (() -> Unit)? = null
onClick: (() -> Unit)? = null,
enabled: Boolean = true
) {
TextRow(
text = {
Text(
TextAndLabel(
text = text,
color = foregroundTint,
modifier = Modifier.align(CenterVertically)
label = label,
textColor = foregroundTint,
enabled = enabled
)
},
icon = if (icon != null) {
@@ -137,22 +155,28 @@ object Rows {
null
},
modifier = modifier,
onClick = onClick
onClick = onClick,
enabled = enabled
)
}
/**
* Customizable text row that allows [text] and [icon] to be provided as composable functions instead of primitives.
*/
@Composable
fun TextRow(
text: @Composable RowScope.() -> Unit,
modifier: Modifier = Modifier,
icon: (@Composable RowScope.() -> Unit)? = null,
onClick: (() -> Unit)? = null
onClick: (() -> Unit)? = null,
enabled: Boolean = true
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable(enabled = onClick != null, onClick = onClick ?: {})
.padding(defaultPadding())
.clickable(enabled = enabled && onClick != null, onClick = onClick ?: {})
.padding(defaultPadding()),
verticalAlignment = CenterVertically
) {
if (icon != null) {
icon()
@@ -169,12 +193,45 @@ object Rows {
vertical = 16.dp
)
}
/**
* Row component to position text above an optional label.
*/
@Composable
fun RowScope.TextAndLabel(
text: String,
modifier: Modifier = Modifier,
label: String? = null,
enabled: Boolean = true,
textColor: Color = MaterialTheme.colorScheme.onSurface,
textStyle: TextStyle = MaterialTheme.typography.bodyLarge
) {
Column(
modifier = modifier
.alpha(if (enabled) 1f else 0.4f)
.weight(1f)
) {
Text(
text = text,
style = textStyle,
color = textColor
)
if (label != null) {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Preview
@SignalPreview
@Composable
private fun RadioRowPreview() {
SignalTheme(isDarkMode = false) {
Previews.Preview {
var selected by remember { mutableStateOf(true) }
Rows.RadioRow(
@@ -188,15 +245,16 @@ private fun RadioRowPreview() {
}
}
@Preview
@SignalPreview
@Composable
private fun ToggleRowPreview() {
SignalTheme(isDarkMode = false) {
Previews.Preview {
var checked by remember { mutableStateOf(false) }
Rows.ToggleRow(
checked = checked,
text = "ToggleRow",
label = "ToggleRow label",
onCheckChanged = {
checked = it
}
@@ -210,7 +268,26 @@ private fun TextRowPreview() {
Previews.Preview {
Rows.TextRow(
text = "TextRow",
icon = painterResource(id = android.R.drawable.ic_menu_camera)
icon = painterResource(id = android.R.drawable.ic_menu_camera),
onClick = {}
)
}
}
@SignalPreview
@Composable
private fun TextAndLabelPreview() {
Previews.Preview {
Row {
TextAndLabel(
text = "TextAndLabel Text",
label = "TextAndLabel Label"
)
TextAndLabel(
text = "TextAndLabel Text",
label = "TextAndLabel Label",
enabled = false
)
}
}
}