mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Convert and update Manage Storage Settings.
This commit is contained in:
committed by
Greyson Parrelli
parent
adef572abb
commit
6d657b449c
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user