mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 03:11:10 +01:00
Add universal disappearing messages.
This commit is contained in:
committed by
Greyson Parrelli
parent
8c6a88374b
commit
defd5e8047
@@ -25,7 +25,7 @@ open class DSLSettingsActivity : PassphraseRequiredActivity() {
|
||||
throw IllegalStateException("No navgraph id was passed to activity")
|
||||
}
|
||||
|
||||
val fragment: NavHostFragment = NavHostFragment.create(navGraphId)
|
||||
val fragment: NavHostFragment = NavHostFragment.create(navGraphId, intent.getBundleExtra(ARG_START_BUNDLE))
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.nav_host_fragment, fragment)
|
||||
@@ -61,6 +61,7 @@ open class DSLSettingsActivity : PassphraseRequiredActivity() {
|
||||
|
||||
companion object {
|
||||
const val ARG_NAV_GRAPH = "nav_graph"
|
||||
const val ARG_START_BUNDLE = "start_bundle"
|
||||
}
|
||||
|
||||
private inner class OnBackPressed : OnBackPressedCallback(true) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.RadioButton
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -26,6 +27,7 @@ class DSLSettingsAdapter : MappingAdapter() {
|
||||
registerFactory(DividerPreference::class.java, LayoutFactory(::DividerPreferenceViewHolder, R.layout.dsl_divider_item))
|
||||
registerFactory(SectionHeaderPreference::class.java, LayoutFactory(::SectionHeaderPreferenceViewHolder, R.layout.dsl_section_header))
|
||||
registerFactory(SwitchPreference::class.java, LayoutFactory(::SwitchPreferenceViewHolder, R.layout.dsl_switch_preference_item))
|
||||
registerFactory(RadioPreference::class.java, LayoutFactory(::RadioPreferenceViewHolder, R.layout.dsl_radio_preference_item))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +152,19 @@ class SwitchPreferenceViewHolder(itemView: View) : PreferenceViewHolder<SwitchPr
|
||||
}
|
||||
}
|
||||
|
||||
class RadioPreferenceViewHolder(itemView: View) : PreferenceViewHolder<RadioPreference>(itemView) {
|
||||
|
||||
private val radioButton: RadioButton = itemView.findViewById(R.id.radio_widget)
|
||||
|
||||
override fun bind(model: RadioPreference) {
|
||||
super.bind(model)
|
||||
radioButton.isChecked = model.isChecked
|
||||
itemView.setOnClickListener {
|
||||
model.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalLinkPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ExternalLinkPreference>(itemView) {
|
||||
override fun bind(model: ExternalLinkPreference) {
|
||||
super.bind(model)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.EdgeEffect
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.MenuRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
@@ -14,8 +15,9 @@ import org.thoughtcrime.securesms.R
|
||||
|
||||
abstract class DSLSettingsFragment(
|
||||
@StringRes private val titleId: Int,
|
||||
@MenuRes private val menuId: Int = -1
|
||||
) : Fragment(R.layout.dsl_settings_fragment) {
|
||||
@MenuRes private val menuId: Int = -1,
|
||||
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment
|
||||
) : Fragment(layoutId) {
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var toolbarShadowHelper: ToolbarShadowHelper
|
||||
|
||||
@@ -6,12 +6,15 @@ import android.content.Intent
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.TextAppearanceSpan
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import mobi.upod.timedurationpicker.TimeDurationPicker
|
||||
@@ -19,10 +22,14 @@ import mobi.upod.timedurationpicker.TimeDurationPickerDialog
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.PassphraseChangeActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.ClickPreference
|
||||
import org.thoughtcrime.securesms.components.settings.ClickPreferenceViewHolder
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
@@ -30,15 +37,17 @@ import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberL
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.lang.Integer.max
|
||||
import java.util.ArrayList
|
||||
import java.util.LinkedHashMap
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
private val TAG = Log.tag(PrivacySettingsFragment::class.java)
|
||||
|
||||
@@ -62,6 +71,8 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
adapter.registerFactory(ValueClickPreference::class.java, MappingAdapter.LayoutFactory(::ValueClickPreferenceViewHolder, R.layout.value_click_preference_item))
|
||||
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val repository = PrivacySettingsRepository()
|
||||
val factory = PrivacySettingsViewModel.Factory(sharedPreferences, repository)
|
||||
@@ -129,6 +140,23 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.PrivacySettingsFragment__disappearing_messages)
|
||||
|
||||
customPref(
|
||||
ValueClickPreference(
|
||||
value = DSLSettingsText.from(ExpirationUtil.getExpirationAbbreviatedDisplayValue(requireContext(), state.universalExpireTimer)),
|
||||
clickPreference = ClickPreference(
|
||||
title = DSLSettingsText.from(R.string.PrivacySettingsFragment__default_timer_for_new_changes),
|
||||
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__set_a_default_disappearing_message_timer_for_all_new_chats_started_by_you),
|
||||
onClick = {
|
||||
NavHostFragment.findNavController(this@PrivacySettingsFragment).navigate(R.id.action_privacySettingsFragment_to_disappearingMessagesTimerSelectFragment)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.PrivacySettingsFragment__app_security)
|
||||
|
||||
if (state.isObsoletePasswordEnabled) {
|
||||
@@ -141,7 +169,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
setTitle(R.string.ApplicationPreferencesActivity_disable_passphrase)
|
||||
setMessage(R.string.ApplicationPreferencesActivity_this_will_permanently_unlock_signal_and_message_notifications)
|
||||
setIcon(R.drawable.ic_warning)
|
||||
setPositiveButton(R.string.ApplicationPreferencesActivity_disable) { dialog, which ->
|
||||
setPositiveButton(R.string.ApplicationPreferencesActivity_disable) { _, _ ->
|
||||
MasterSecretUtil.changeMasterSecretPassphrase(
|
||||
activity,
|
||||
KeyCachingService.getMasterSecret(context),
|
||||
@@ -395,4 +423,31 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private class ValueClickPreference(
|
||||
val value: DSLSettingsText,
|
||||
val clickPreference: ClickPreference
|
||||
) : PreferenceModel<ValueClickPreference>(
|
||||
title = clickPreference.title,
|
||||
summary = clickPreference.summary,
|
||||
iconId = clickPreference.iconId,
|
||||
isEnabled = clickPreference.isEnabled
|
||||
) {
|
||||
override fun areContentsTheSame(newItem: ValueClickPreference): Boolean {
|
||||
return super.areContentsTheSame(newItem) &&
|
||||
clickPreference == newItem.clickPreference &&
|
||||
value == newItem.value
|
||||
}
|
||||
}
|
||||
|
||||
private class ValueClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ValueClickPreference>(itemView) {
|
||||
private val clickPreferenceViewHolder = ClickPreferenceViewHolder(itemView)
|
||||
private val valueText: TextView = findViewById(R.id.value_client_preference_value)
|
||||
|
||||
override fun bind(model: ValueClickPreference) {
|
||||
super.bind(model)
|
||||
clickPreferenceViewHolder.bind(model.clickPreference)
|
||||
valueText.text = model.value.resolve(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@ data class PrivacySettingsState(
|
||||
val incognitoKeyboard: Boolean,
|
||||
val isObsoletePasswordEnabled: Boolean,
|
||||
val isObsoletePasswordTimeoutEnabled: Boolean,
|
||||
val obsoletePasswordTimeout: Int
|
||||
val obsoletePasswordTimeout: Int,
|
||||
val universalExpireTimer: Int
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ class PrivacySettingsViewModel(
|
||||
fun refreshBlockedCount() {
|
||||
repository.getBlockedCount { count ->
|
||||
store.update { it.copy(blockedCount = count) }
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +100,8 @@ class PrivacySettingsViewModel(
|
||||
findMeByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode,
|
||||
isObsoletePasswordEnabled = !TextSecurePreferences.isPasswordDisabled(ApplicationDependencies.getApplication()),
|
||||
isObsoletePasswordTimeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(ApplicationDependencies.getApplication()),
|
||||
obsoletePasswordTimeout = TextSecurePreferences.getPassphraseTimeoutInterval(ApplicationDependencies.getApplication())
|
||||
obsoletePasswordTimeout = TextSecurePreferences.getPassphraseTimeoutInterval(ApplicationDependencies.getApplication()),
|
||||
universalExpireTimer = SignalStore.settings().universalExpireTimer
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Dialog for selecting a custom expire timer value.
|
||||
*/
|
||||
class CustomExpireTimerSelectDialog : DialogFragment() {
|
||||
|
||||
private lateinit var viewModel: ExpireTimerSettingsViewModel
|
||||
private lateinit var selector: CustomExpireTimerSelectorView
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialogView: View = LayoutInflater.from(context).inflate(R.layout.custom_expire_timer_select_dialog, null, false)
|
||||
selector = dialogView.findViewById(R.id.custom_expire_timer_select_dialog_selector)
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(requireContext(), R.style.Signal_ThemeOverlay_Dialog_Rounded)
|
||||
|
||||
return builder.setTitle(R.string.ExpireTimerSettingsFragment__custom_time)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.ExpireTimerSettingsFragment__set) { _, _ ->
|
||||
viewModel.select(selector.getTimer())
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
viewModel = ViewModelProvider(NavHostFragment.findNavController(this).getViewModelStoreOwner(R.id.app_settings_expire_timer))
|
||||
.get(ExpireTimerSettingsViewModel::class.java)
|
||||
|
||||
viewModel.state.observe(this) { selector.setTimer(it.currentTimer) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "CustomTimerSelectDialog"
|
||||
|
||||
fun show(fragmentManager: FragmentManager) {
|
||||
CustomExpireTimerSelectDialog().show(fragmentManager, DIALOG_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.NumberPicker
|
||||
import org.thoughtcrime.securesms.R
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Show number pickers for value and units that are valid for expiration timer.
|
||||
*/
|
||||
class CustomExpireTimerSelectorView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val valuePicker: NumberPicker
|
||||
private val unitPicker: NumberPicker
|
||||
|
||||
init {
|
||||
orientation = HORIZONTAL
|
||||
gravity = Gravity.CENTER
|
||||
inflate(context, R.layout.custom_expire_timer_selector_view, this)
|
||||
|
||||
valuePicker = findViewById(R.id.custom_expire_timer_selector_value)
|
||||
unitPicker = findViewById(R.id.custom_expire_timer_selector_unit)
|
||||
|
||||
valuePicker.minValue = TimerUnit.get(1).minValue
|
||||
valuePicker.maxValue = TimerUnit.get(1).maxValue
|
||||
|
||||
unitPicker.minValue = 0
|
||||
unitPicker.maxValue = 4
|
||||
unitPicker.value = 1
|
||||
unitPicker.wrapSelectorWheel = false
|
||||
unitPicker.isLongClickable = false
|
||||
unitPicker.displayedValues = context.resources.getStringArray(R.array.CustomExpireTimerSelectorView__unit_labels)
|
||||
unitPicker.setOnValueChangedListener { _, _, newValue -> unitChange(newValue) }
|
||||
}
|
||||
|
||||
fun setTimer(timer: Int?) {
|
||||
if (timer == null || timer == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
TimerUnit.values()
|
||||
.find { (timer / it.valueMultiplier) < it.maxValue }
|
||||
?.let { timerUnit ->
|
||||
valuePicker.value = (timer / timerUnit.valueMultiplier).toInt()
|
||||
unitPicker.value = TimerUnit.values().indexOf(timerUnit)
|
||||
unitChange(unitPicker.value)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTimer(): Int {
|
||||
return valuePicker.value * TimerUnit.get(unitPicker.value).valueMultiplier.toInt()
|
||||
}
|
||||
|
||||
private fun unitChange(newValue: Int) {
|
||||
val timerUnit: TimerUnit = TimerUnit.values()[newValue]
|
||||
|
||||
valuePicker.minValue = timerUnit.minValue
|
||||
valuePicker.maxValue = timerUnit.maxValue
|
||||
}
|
||||
|
||||
private enum class TimerUnit(val minValue: Int, val maxValue: Int, val valueMultiplier: Long) {
|
||||
SECONDS(1, 59, TimeUnit.SECONDS.toSeconds(1)),
|
||||
MINUTES(1, 59, TimeUnit.MINUTES.toSeconds(1)),
|
||||
HOURS(1, 23, TimeUnit.HOURS.toSeconds(1)),
|
||||
DAYS(1, 6, TimeUnit.DAYS.toSeconds(1)),
|
||||
WEEKS(1, 4, TimeUnit.DAYS.toSeconds(7));
|
||||
|
||||
companion object {
|
||||
fun get(value: Int) = values()[value]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.dd.CircularProgressButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.ProcessState
|
||||
import org.thoughtcrime.securesms.util.livedata.distinctUntilChanged
|
||||
|
||||
/**
|
||||
* Depending on the arguments, can be used to set the universal expire timer, set expire timer
|
||||
* for a individual or group recipient, or select a value and return it via result.
|
||||
*/
|
||||
class ExpireTimerSettingsFragment : DSLSettingsFragment(
|
||||
titleId = R.string.PrivacySettingsFragment__disappearing_messages,
|
||||
layoutId = R.layout.expire_timer_settings_fragment
|
||||
) {
|
||||
|
||||
private lateinit var save: CircularProgressButton
|
||||
private lateinit var viewModel: ExpireTimerSettingsViewModel
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
save = view.findViewById(R.id.timer_select_fragment_save)
|
||||
save.setOnClickListener { viewModel.save() }
|
||||
adjustListPaddingForSaveButton(view)
|
||||
}
|
||||
|
||||
private fun adjustListPaddingForSaveButton(view: View) {
|
||||
val recycler: RecyclerView = view.findViewById(R.id.recycler)
|
||||
recycler.setPadding(recycler.paddingLeft, recycler.paddingTop, recycler.paddingRight, ViewUtil.dpToPx(80))
|
||||
recycler.clipToPadding = false
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
val provider = ViewModelProvider(
|
||||
NavHostFragment.findNavController(this).getViewModelStoreOwner(R.id.app_settings_expire_timer),
|
||||
ExpireTimerSettingsViewModel.Factory(requireContext(), arguments.toConfig())
|
||||
)
|
||||
viewModel = provider.get(ExpireTimerSettingsViewModel::class.java)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
|
||||
viewModel.state.distinctUntilChanged(ExpireTimerSettingsState::saveState).observe(viewLifecycleOwner) { state ->
|
||||
when (val saveState: ProcessState<Int> = state.saveState) {
|
||||
is ProcessState.Working -> {
|
||||
save.isClickable = false
|
||||
save.isIndeterminateProgressMode = true
|
||||
save.progress = 50
|
||||
}
|
||||
is ProcessState.Success -> {
|
||||
if (state.isGroupCreate) {
|
||||
requireActivity().setResult(Activity.RESULT_OK, Intent().putExtra(FOR_RESULT_VALUE, saveState.result))
|
||||
}
|
||||
save.isClickable = false
|
||||
requireActivity().onNavigateUp()
|
||||
}
|
||||
is ProcessState.Failure -> {
|
||||
val groupChangeFailureReason: GroupChangeFailureReason = saveState.throwable?.let(GroupChangeFailureReason::fromException) ?: GroupChangeFailureReason.OTHER
|
||||
Toast.makeText(context, GroupErrors.getUserDisplayMessage(groupChangeFailureReason), Toast.LENGTH_LONG).show()
|
||||
viewModel.resetError()
|
||||
}
|
||||
else -> {
|
||||
save.isClickable = true
|
||||
save.isIndeterminateProgressMode = false
|
||||
save.progress = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: ExpireTimerSettingsState): DSLConfiguration {
|
||||
return configure {
|
||||
textPref(
|
||||
summary = DSLSettingsText.from(
|
||||
if (state.isForRecipient) {
|
||||
R.string.ExpireTimerSettingsFragment__when_enabled_new_messages_sent_and_received_in_this_chat_will_disappear_after_they_have_been_seen
|
||||
} else {
|
||||
R.string.ExpireTimerSettingsFragment__when_enabled_new_messages_sent_and_received_in_new_chats_started_by_you_will_disappear_after_they_have_been_seen
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val labels: Array<String> = resources.getStringArray(R.array.ExpireTimerSettingsFragment__labels)
|
||||
val values: Array<Int> = resources.getIntArray(R.array.ExpireTimerSettingsFragment__values).toTypedArray()
|
||||
|
||||
var hasCustomValue = true
|
||||
labels.zip(values).forEach { (label, value) ->
|
||||
radioPref(
|
||||
title = DSLSettingsText.from(label),
|
||||
isChecked = state.currentTimer == value,
|
||||
onClick = { viewModel.select(value) }
|
||||
)
|
||||
hasCustomValue = hasCustomValue && state.currentTimer != value
|
||||
}
|
||||
|
||||
radioPref(
|
||||
title = DSLSettingsText.from(R.string.ExpireTimerSettingsFragment__custom_time),
|
||||
summary = if (hasCustomValue) DSLSettingsText.from(ExpirationUtil.getExpirationDisplayValue(requireContext(), state.currentTimer)) else null,
|
||||
isChecked = hasCustomValue,
|
||||
onClick = { NavHostFragment.findNavController(this@ExpireTimerSettingsFragment).navigate(R.id.action_expireTimerSettingsFragment_to_customExpireTimerSelectDialog) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FOR_RESULT_VALUE = "for_result_value"
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bundle?.toConfig(): ExpireTimerSettingsViewModel.Config {
|
||||
if (this == null) {
|
||||
return ExpireTimerSettingsViewModel.Config()
|
||||
}
|
||||
|
||||
val safeArguments: ExpireTimerSettingsFragmentArgs = ExpireTimerSettingsFragmentArgs.fromBundle(this)
|
||||
return ExpireTimerSettingsViewModel.Config(
|
||||
recipientId = safeArguments.recipientId,
|
||||
forResultMode = safeArguments.forResultMode,
|
||||
initialValue = safeArguments.initialValue
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeException
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import java.io.IOException
|
||||
|
||||
private val TAG: String = Log.tag(ExpireTimerSettingsRepository::class.java)
|
||||
|
||||
/**
|
||||
* Provide operations to set expire timer for individuals and groups.
|
||||
*/
|
||||
class ExpireTimerSettingsRepository(val context: Context) {
|
||||
|
||||
fun setExpiration(recipientId: RecipientId, newExpirationTime: Int, consumer: (Result<Int>) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
if (recipient.groupId.isPresent && recipient.groupId.get().isPush) {
|
||||
try {
|
||||
GroupManager.updateGroupTimer(context, recipient.groupId.get().requirePush(), newExpirationTime)
|
||||
consumer.invoke(Result.success(newExpirationTime))
|
||||
} catch (e: GroupChangeException) {
|
||||
Log.w(TAG, e)
|
||||
consumer.invoke(Result.failure(e))
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
consumer.invoke(Result.failure(e))
|
||||
}
|
||||
} else {
|
||||
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipientId, newExpirationTime)
|
||||
val outgoingMessage = OutgoingExpirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L)
|
||||
MessageSender.send(context, outgoingMessage, getThreadId(recipientId), false, null)
|
||||
consumer.invoke(Result.success(newExpirationTime))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun getThreadId(recipientId: RecipientId): Long {
|
||||
val threadDatabase: ThreadDatabase = DatabaseFactory.getThreadDatabase(context)
|
||||
val recipient: Recipient = Recipient.resolved(recipientId)
|
||||
return threadDatabase.getThreadIdFor(recipient)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||
|
||||
import org.thoughtcrime.securesms.util.livedata.ProcessState
|
||||
|
||||
data class ExpireTimerSettingsState(
|
||||
val initialTimer: Int = 0,
|
||||
val userSetTimer: Int? = null,
|
||||
val saveState: ProcessState<Int> = ProcessState.Idle(),
|
||||
val isGroupCreate: Boolean = false,
|
||||
val isForRecipient: Boolean = isGroupCreate,
|
||||
) {
|
||||
val currentTimer: Int
|
||||
get() = userSetTimer ?: initialTimer
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.livedata.ProcessState
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class ExpireTimerSettingsViewModel(val config: Config, private val repository: ExpireTimerSettingsRepository) : ViewModel() {
|
||||
|
||||
private val store = Store<ExpireTimerSettingsState>(ExpireTimerSettingsState(isGroupCreate = config.forResultMode))
|
||||
private val recipientId: RecipientId? = config.recipientId
|
||||
|
||||
val state: LiveData<ExpireTimerSettingsState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
if (recipientId != null) {
|
||||
store.update(Recipient.live(recipientId).liveData) { r, s -> s.copy(initialTimer = r.expireMessages, isForRecipient = true) }
|
||||
} else {
|
||||
store.update { it.copy(initialTimer = config.initialValue ?: SignalStore.settings().universalExpireTimer) }
|
||||
}
|
||||
}
|
||||
|
||||
fun select(time: Int) {
|
||||
store.update { it.copy(userSetTimer = time) }
|
||||
}
|
||||
|
||||
fun save() {
|
||||
val userSetTimer: Int = store.state.currentTimer
|
||||
|
||||
if (userSetTimer == store.state.initialTimer) {
|
||||
store.update { it.copy(saveState = ProcessState.Success(userSetTimer)) }
|
||||
return
|
||||
}
|
||||
|
||||
store.update { it.copy(saveState = ProcessState.Working()) }
|
||||
if (recipientId != null) {
|
||||
repository.setExpiration(recipientId, userSetTimer) { result ->
|
||||
store.update { it.copy(saveState = ProcessState.fromResult(result)) }
|
||||
}
|
||||
} else if (config.forResultMode) {
|
||||
store.update { it.copy(saveState = ProcessState.Success(userSetTimer)) }
|
||||
} else {
|
||||
SignalStore.settings().universalExpireTimer = userSetTimer
|
||||
store.update { it.copy(saveState = ProcessState.Success(userSetTimer)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun resetError() {
|
||||
store.update { it.copy(saveState = ProcessState.Idle()) }
|
||||
}
|
||||
|
||||
class Factory(context: Context, private val config: Config) : ViewModelProvider.Factory {
|
||||
val repository = ExpireTimerSettingsRepository(context.applicationContext)
|
||||
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(ExpireTimerSettingsViewModel(config, repository)))
|
||||
}
|
||||
}
|
||||
|
||||
data class Config(
|
||||
val recipientId: RecipientId? = null,
|
||||
val forResultMode: Boolean = false,
|
||||
val initialValue: Int? = null
|
||||
)
|
||||
}
|
||||
@@ -56,6 +56,17 @@ class DSLConfiguration {
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun radioPref(
|
||||
title: DSLSettingsText,
|
||||
summary: DSLSettingsText? = null,
|
||||
isEnabled: Boolean = true,
|
||||
isChecked: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val preference = RadioPreference(title, summary, isEnabled, isChecked, onClick)
|
||||
children.add(preference)
|
||||
}
|
||||
|
||||
fun clickPref(
|
||||
title: DSLSettingsText,
|
||||
summary: DSLSettingsText? = null,
|
||||
@@ -175,11 +186,23 @@ class SwitchPreference(
|
||||
}
|
||||
}
|
||||
|
||||
class RadioPreference(
|
||||
title: DSLSettingsText,
|
||||
summary: DSLSettingsText? = null,
|
||||
isEnabled: Boolean,
|
||||
val isChecked: Boolean,
|
||||
val onClick: () -> Unit
|
||||
) : PreferenceModel<RadioPreference>(title = title, summary = summary, isEnabled = isEnabled) {
|
||||
override fun areContentsTheSame(newItem: RadioPreference): Boolean {
|
||||
return super.areContentsTheSame(newItem) && isChecked == newItem.isChecked
|
||||
}
|
||||
}
|
||||
|
||||
class ClickPreference(
|
||||
override val title: DSLSettingsText,
|
||||
override val summary: DSLSettingsText?,
|
||||
@DrawableRes override val iconId: Int,
|
||||
isEnabled: Boolean,
|
||||
override val summary: DSLSettingsText? = null,
|
||||
@DrawableRes override val iconId: Int = UNSET,
|
||||
isEnabled: Boolean = true,
|
||||
val onClick: () -> Unit
|
||||
) : PreferenceModel<ClickPreference>(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user