diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4a7a7a300..4c49241bf 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -305,6 +305,11 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+
+
(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(itemView) {
override fun bind(model: ExternalLinkPreference) {
super.bind(model)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt
index 7897d7fcb..a29b6431e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt
@@ -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
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt
index 795629af8..947d3fb42 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt
@@ -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(
+ 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(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)
+ }
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsState.kt
index 4bab0dd51..14f6e5591 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsState.kt
@@ -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
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt
index 63bbe956e..d68d0c148 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt
@@ -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
)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/CustomExpireTimerSelectDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/CustomExpireTimerSelectDialog.kt
new file mode 100644
index 000000000..7e8078daf
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/CustomExpireTimerSelectDialog.kt
@@ -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)
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/CustomExpireTimerSelectorView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/CustomExpireTimerSelectorView.kt
new file mode 100644
index 000000000..c5b87cb13
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/CustomExpireTimerSelectorView.kt
@@ -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]
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsFragment.kt
new file mode 100644
index 000000000..aa4973a68
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsFragment.kt
@@ -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 = 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 = resources.getStringArray(R.array.ExpireTimerSettingsFragment__labels)
+ val values: Array = 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
+ )
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsRepository.kt
new file mode 100644
index 000000000..85af24ef0
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsRepository.kt
@@ -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) -> 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)
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsState.kt
new file mode 100644
index 000000000..732a08307
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsState.kt
@@ -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 = ProcessState.Idle(),
+ val isGroupCreate: Boolean = false,
+ val isForRecipient: Boolean = isGroupCreate,
+) {
+ val currentTimer: Int
+ get() = userSetTimer ?: initialTimer
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsViewModel.kt
new file mode 100644
index 000000000..45bb12c96
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/ExpireTimerSettingsViewModel.kt
@@ -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(isGroupCreate = config.forResultMode))
+ private val recipientId: RecipientId? = config.recipientId
+
+ val state: LiveData = 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 create(modelClass: Class): T {
+ return requireNotNull(modelClass.cast(ExpireTimerSettingsViewModel(config, repository)))
+ }
+ }
+
+ data class Config(
+ val recipientId: RecipientId? = null,
+ val forResultMode: Boolean = false,
+ val initialValue: Int? = null
+ )
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt
index 315af6fec..d35f5df3e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt
@@ -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(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(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index a2ec94d6f..599c2fbae 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -2412,6 +2412,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (groupCallViewModel != null) {
groupCallViewModel.onRecipientChange(recipient);
}
+
+ if (this.threadId == -1) {
+ SimpleTask.run(() -> DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId()), threadId -> {
+ if (this.threadId != threadId) {
+ Log.d(TAG, "Thread id changed via recipient change");
+ this.threadId = threadId;
+ fragment.reload(recipient, this.threadId);
+ setVisibleThread(this.threadId);
+ }
+ });
+ }
}
@Subscribe(threadMode = ThreadMode.MAIN)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java
index 9e5e9d407..539388593 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java
@@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
@@ -374,6 +375,10 @@ public class ConversationAdapter
this.pagingController = pagingController;
}
+ public boolean isForRecipientId(@NonNull RecipientId recipientId) {
+ return recipient.getId().equals(recipientId);
+ }
+
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java
index 176d7aad6..b3a5283f6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java
@@ -14,6 +14,7 @@ final class ConversationData {
private final int jumpToPosition;
private final int threadSize;
private final MessageRequestData messageRequestData;
+ private final boolean showUniversalExpireTimerMessage;
ConversationData(long threadId,
long lastSeen,
@@ -22,16 +23,18 @@ final class ConversationData {
boolean hasSent,
int jumpToPosition,
int threadSize,
- @NonNull MessageRequestData messageRequestData)
+ @NonNull MessageRequestData messageRequestData,
+ boolean showUniversalExpireTimerMessage)
{
- this.threadId = threadId;
- this.lastSeen = lastSeen;
- this.lastSeenPosition = lastSeenPosition;
- this.lastScrolledPosition = lastScrolledPosition;
- this.hasSent = hasSent;
- this.jumpToPosition = jumpToPosition;
- this.threadSize = threadSize;
- this.messageRequestData = messageRequestData;
+ this.threadId = threadId;
+ this.lastSeen = lastSeen;
+ this.lastSeenPosition = lastSeenPosition;
+ this.lastScrolledPosition = lastScrolledPosition;
+ this.hasSent = hasSent;
+ this.jumpToPosition = jumpToPosition;
+ this.threadSize = threadSize;
+ this.messageRequestData = messageRequestData;
+ this.showUniversalExpireTimerMessage = showUniversalExpireTimerMessage;
}
public long getThreadId() {
@@ -74,6 +77,10 @@ final class ConversationData {
return messageRequestData;
}
+ public boolean showUniversalExpireTimerMessage() {
+ return showUniversalExpireTimerMessage;
+ }
+
static final class MessageRequestData {
private final boolean messageRequestAccepted;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java
index 427f248a7..8b810d48c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java
@@ -35,17 +35,21 @@ class ConversationDataSource implements PagedDataSource {
private final Context context;
private final long threadId;
private final MessageRequestData messageRequestData;
+ private final boolean showUniversalExpireTimerUpdate;
- ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData) {
- this.context = context;
- this.threadId = threadId;
- this.messageRequestData = messageRequestData;
+ ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData, boolean showUniversalExpireTimerUpdate) {
+ this.context = context;
+ this.threadId = threadId;
+ this.messageRequestData = messageRequestData;
+ this.showUniversalExpireTimerUpdate = showUniversalExpireTimerUpdate;
}
@Override
public int size() {
long startTime = System.currentTimeMillis();
- int size = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId) + (messageRequestData.includeWarningUpdateMessage() ? 1 : 0);
+ int size = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId) +
+ (messageRequestData.includeWarningUpdateMessage() ? 1 : 0) +
+ (showUniversalExpireTimerUpdate ? 1 : 0);
Log.d(TAG, "size() for thread " + threadId + ": " + (System.currentTimeMillis() - startTime) + " ms");
@@ -71,6 +75,10 @@ class ConversationDataSource implements PagedDataSource {
records.add(new InMemoryMessageRecord.NoGroupsInCommon(threadId, messageRequestData.isGroup()));
}
+ if (showUniversalExpireTimerUpdate) {
+ records.add(new InMemoryMessageRecord.UniversalExpireTimerUpdate(threadId));
+ }
+
stopwatch.split("messages");
mentionHelper.fetchMentions(context);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
index ac8efd921..f8e25c7ab 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
@@ -183,38 +183,37 @@ public class ConversationFragment extends LoggingFragment {
private ConversationFragmentListener listener;
- private LiveRecipient recipient;
- private long threadId;
- private boolean isReacting;
- private ActionMode actionMode;
- private Locale locale;
- private FrameLayout videoContainer;
- private RecyclerView list;
- private RecyclerView.ItemDecoration lastSeenDecoration;
- private RecyclerView.ItemDecoration inlineDateDecoration;
- private ViewSwitcher topLoadMoreView;
- private ViewSwitcher bottomLoadMoreView;
- private ConversationTypingView typingView;
- private View composeDivider;
- private ConversationScrollToView scrollToBottomButton;
- private ConversationScrollToView scrollToMentionButton;
- private TextView scrollDateHeader;
- private ConversationBannerView conversationBanner;
- private ConversationBannerView emptyConversationBanner;
- private MessageRequestViewModel messageRequestViewModel;
- private MessageCountsViewModel messageCountsViewModel;
- private ConversationViewModel conversationViewModel;
- private SnapToTopDataObserver snapToTopDataObserver;
- private MarkReadHelper markReadHelper;
- private Animation scrollButtonInAnimation;
- private Animation mentionButtonInAnimation;
- private Animation scrollButtonOutAnimation;
- private Animation mentionButtonOutAnimation;
- private OnScrollListener conversationScrollListener;
- private int pulsePosition = -1;
- private VoiceNoteMediaController voiceNoteMediaController;
- private View toolbarShadow;
- private Stopwatch startupStopwatch;
+ private LiveRecipient recipient;
+ private long threadId;
+ private boolean isReacting;
+ private ActionMode actionMode;
+ private Locale locale;
+ private FrameLayout videoContainer;
+ private RecyclerView list;
+ private RecyclerView.ItemDecoration lastSeenDecoration;
+ private RecyclerView.ItemDecoration inlineDateDecoration;
+ private ViewSwitcher topLoadMoreView;
+ private ViewSwitcher bottomLoadMoreView;
+ private ConversationTypingView typingView;
+ private View composeDivider;
+ private ConversationScrollToView scrollToBottomButton;
+ private ConversationScrollToView scrollToMentionButton;
+ private TextView scrollDateHeader;
+ private ConversationBannerView conversationBanner;
+ private MessageRequestViewModel messageRequestViewModel;
+ private MessageCountsViewModel messageCountsViewModel;
+ private ConversationViewModel conversationViewModel;
+ private SnapToTopDataObserver snapToTopDataObserver;
+ private MarkReadHelper markReadHelper;
+ private Animation scrollButtonInAnimation;
+ private Animation mentionButtonInAnimation;
+ private Animation scrollButtonOutAnimation;
+ private Animation mentionButtonOutAnimation;
+ private OnScrollListener conversationScrollListener;
+ private int pulsePosition = -1;
+ private VoiceNoteMediaController voiceNoteMediaController;
+ private View toolbarShadow;
+ private Stopwatch startupStopwatch;
private GiphyMp4ProjectionRecycler giphyMp4ProjectionRecycler;
@@ -240,15 +239,14 @@ public class ConversationFragment extends LoggingFragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
- videoContainer = view.findViewById(R.id.video_container);
- list = view.findViewById(android.R.id.list);
- composeDivider = view.findViewById(R.id.compose_divider);
+ videoContainer = view.findViewById(R.id.video_container);
+ list = view.findViewById(android.R.id.list);
+ composeDivider = view.findViewById(R.id.compose_divider);
- scrollToBottomButton = view.findViewById(R.id.scroll_to_bottom);
- scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
- scrollDateHeader = view.findViewById(R.id.scroll_date_header);
- emptyConversationBanner = view.findViewById(R.id.empty_conversation_banner);
- toolbarShadow = requireActivity().findViewById(R.id.conversation_toolbar_shadow);
+ scrollToBottomButton = view.findViewById(R.id.scroll_to_bottom);
+ scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
+ scrollDateHeader = view.findViewById(R.id.scroll_date_header);
+ toolbarShadow = requireActivity().findViewById(R.id.conversation_toolbar_shadow);
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
list.setHasFixedSize(false);
@@ -483,7 +481,6 @@ public class ConversationFragment extends LoggingFragment {
messageRequestViewModel.getRecipientInfo().observe(getViewLifecycleOwner(), recipientInfo -> {
presentMessageRequestProfileView(requireContext(), recipientInfo, conversationBanner);
- presentMessageRequestProfileView(requireContext(), recipientInfo, emptyConversationBanner);
});
messageRequestViewModel.getMessageData().observe(getViewLifecycleOwner(), data -> {
@@ -606,7 +603,16 @@ public class ConversationFragment extends LoggingFragment {
}
private void initializeListAdapter() {
- if (this.recipient != null && this.threadId != -1) {
+ if (threadId == -1) {
+ toolbarShadow.setVisibility(View.GONE);
+ }
+
+ if (this.recipient != null) {
+ if (getListAdapter() != null && getListAdapter().isForRecipientId(this.recipient.getId())) {
+ Log.d(TAG, "List adapter already initialized for " + this.recipient.getId());
+ return;
+ }
+
Log.d(TAG, "Initializing adapter for " + recipient.getId());
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get(), new AttachmentMediaSourceFactory(requireContext()));
adapter.setPagingController(conversationViewModel.getPagingController());
@@ -618,8 +624,6 @@ public class ConversationFragment extends LoggingFragment {
setLastSeen(conversationViewModel.getLastSeen());
- emptyConversationBanner.setVisibility(View.GONE);
-
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
@@ -631,9 +635,6 @@ public class ConversationFragment extends LoggingFragment {
});
}
});
- } else if (threadId == -1) {
- emptyConversationBanner.setVisibility(View.VISIBLE);
- toolbarShadow.setVisibility(View.GONE);
}
}
@@ -726,6 +727,7 @@ public class ConversationFragment extends LoggingFragment {
menu.findItem(R.id.menu_context_save_attachment).setVisible(menuState.shouldShowSaveAttachmentAction());
menu.findItem(R.id.menu_context_resend).setVisible(menuState.shouldShowResendAction());
menu.findItem(R.id.menu_context_copy).setVisible(menuState.shouldShowCopyAction());
+ menu.findItem(R.id.menu_context_delete_message).setVisible(menuState.shouldShowDeleteAction());
}
private @Nullable ConversationAdapter getListAdapter() {
@@ -756,7 +758,9 @@ public class ConversationFragment extends LoggingFragment {
snapToTopDataObserver.requestScrollPosition(0);
conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, -1);
messageCountsViewModel.setThreadId(threadId);
+ markReadHelper = new MarkReadHelper(threadId, requireContext());
initializeListAdapter();
+ initializeTypingObserver();
}
}
@@ -1229,7 +1233,6 @@ public class ConversationFragment extends LoggingFragment {
toolbar.getGlobalVisibleRect(rect);
ViewUtil.setTopMargin(scrollDateHeader, rect.bottom + ViewUtil.dpToPx(8));
ViewUtil.setTopMargin(conversationBanner, rect.bottom + ViewUtil.dpToPx(16));
- ViewUtil.setTopMargin(emptyConversationBanner, rect.bottom + ViewUtil.dpToPx(16));
toolbar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java
index 14655907e..672ed0eb6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java
@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.BubbleUtil;
@@ -32,11 +33,11 @@ class ConversationRepository {
this.executor = SignalExecutors.BOUNDED;
}
- LiveData getConversationData(long threadId, int jumpToPosition) {
+ LiveData getConversationData(long threadId, @NonNull Recipient recipient, int jumpToPosition) {
MutableLiveData liveData = new MutableLiveData<>();
executor.execute(() -> {
- liveData.postValue(getConversationDataInternal(threadId, jumpToPosition));
+ liveData.postValue(getConversationDataInternal(threadId, recipient, jumpToPosition));
});
return liveData;
@@ -53,16 +54,17 @@ class ConversationRepository {
}
}
- private @NonNull ConversationData getConversationDataInternal(long threadId, int jumpToPosition) {
- ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId);
- int threadSize = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId);
- long lastSeen = metadata.getLastSeen();
- boolean hasSent = metadata.hasSent();
- int lastSeenPosition = 0;
- long lastScrolled = metadata.getLastScrolled();
- int lastScrolledPosition = 0;
- boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId);
- ConversationData.MessageRequestData messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted);
+ private @NonNull ConversationData getConversationDataInternal(long threadId, @NonNull Recipient conversationRecipient, int jumpToPosition) {
+ ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId);
+ int threadSize = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId);
+ long lastSeen = metadata.getLastSeen();
+ boolean hasSent = metadata.hasSent();
+ int lastSeenPosition = 0;
+ long lastScrolled = metadata.getLastScrolled();
+ int lastScrolledPosition = 0;
+ boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId);
+ ConversationData.MessageRequestData messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted);
+ boolean showUniversalExpireTimerUpdate = false;
if (lastSeen > 0) {
lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastSeen);
@@ -79,9 +81,8 @@ class ConversationRepository {
if (!isMessageRequestAccepted) {
boolean isGroup = false;
boolean recipientIsKnownOrHasGroupsInCommon = false;
- Recipient threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
- if (threadRecipient.isGroup()) {
- Optional group = DatabaseFactory.getGroupDatabase(context).getGroup(threadRecipient.getId());
+ if (conversationRecipient.isGroup()) {
+ Optional group = DatabaseFactory.getGroupDatabase(context).getGroup(conversationRecipient.getId());
if (group.isPresent()) {
List recipients = Recipient.resolvedList(group.get().getMembers());
for (Recipient recipient : recipients) {
@@ -92,12 +93,20 @@ class ConversationRepository {
}
}
isGroup = true;
- } else if (threadRecipient.hasGroupsInCommon()) {
+ } else if (conversationRecipient.hasGroupsInCommon()) {
recipientIsKnownOrHasGroupsInCommon = true;
}
messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted, recipientIsKnownOrHasGroupsInCommon, isGroup);
}
- return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, jumpToPosition, threadSize, messageRequestData);
+ if (SignalStore.settings().getUniversalExpireTimer() != 0 &&
+ conversationRecipient.getExpireMessages() == 0 &&
+ !conversationRecipient.isGroup() &&
+ (threadId == -1 || !DatabaseFactory.getMmsSmsDatabase(context).hasMeaningfulMessage(threadId)))
+ {
+ showUniversalExpireTimerUpdate = true;
+ }
+
+ return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java
index 950ddfc72..cf66fc06a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java
@@ -69,8 +69,11 @@ public class ConversationViewModel extends ViewModel {
this.pagingController = new ProxyPagingController();
this.messageObserver = pagingController::onDataInvalidated;
- LiveData metadata = Transformations.switchMap(threadId, thread -> {
- LiveData conversationData = conversationRepository.getConversationData(thread, jumpToPosition);
+ LiveData recipientLiveData = LiveDataUtil.mapAsync(recipientId, Recipient::resolved);
+ LiveData threadAndRecipient = LiveDataUtil.combineLatest(threadId, recipientLiveData, ThreadAndRecipient::new);
+
+ LiveData metadata = Transformations.switchMap(threadAndRecipient, d -> {
+ LiveData conversationData = conversationRepository.getConversationData(d.threadId, d.recipient, jumpToPosition);
jumpToPosition = -1;
@@ -94,12 +97,11 @@ public class ConversationViewModel extends ViewModel {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver);
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), messageObserver);
- ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData);
- PagingConfig config = new PagingConfig.Builder()
- .setPageSize(25)
- .setBufferPages(3)
- .setStartIndex(Math.max(startPosition, 0))
- .build();
+ ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData, data.showUniversalExpireTimerMessage());
+ PagingConfig config = new PagingConfig.Builder().setPageSize(25)
+ .setBufferPages(3)
+ .setStartIndex(Math.max(startPosition, 0))
+ .build();
Log.d(TAG, "Starting at position: " + startPosition + " || jumpToPosition: " + data.getJumpToPosition() + ", lastSeenPosition: " + data.getLastSeenPosition() + ", lastScrolledPosition: " + data.getLastScrolledPosition());
return new Pair<>(data.getThreadId(), PagedData.create(dataSource, config));
@@ -213,9 +215,20 @@ public class ConversationViewModel extends ViewModel {
SHOW_RECAPTCHA
}
+ private static class ThreadAndRecipient {
+
+ private final long threadId;
+ private final Recipient recipient;
+
+ public ThreadAndRecipient(long threadId, Recipient recipient) {
+ this.threadId = threadId;
+ this.recipient = recipient;
+ }
+ }
+
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
- public @NonNull T create(@NonNull Class modelClass) {
+ public @NonNull T create(@NonNull Class modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationViewModel());
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java
index e7fe69b1d..7bd9eaed8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java
@@ -17,6 +17,7 @@ final class MenuState {
private final boolean saveAttachment;
private final boolean resend;
private final boolean copy;
+ private final boolean delete;
private MenuState(@NonNull Builder builder) {
forward = builder.forward;
@@ -25,6 +26,7 @@ final class MenuState {
saveAttachment = builder.saveAttachment;
resend = builder.resend;
copy = builder.copy;
+ delete = builder.delete;
}
boolean shouldShowForwardAction() {
@@ -51,6 +53,10 @@ final class MenuState {
return copy;
}
+ boolean shouldShowDeleteAction() {
+ return delete;
+ }
+
static MenuState getMenuState(@NonNull Recipient conversationRecipient,
@NonNull Set messageRecords,
boolean shouldShowMessageRequest)
@@ -62,11 +68,14 @@ final class MenuState {
boolean sharedContact = false;
boolean viewOnce = false;
boolean remoteDelete = false;
+ boolean hasInMemory = false;
for (MessageRecord messageRecord : messageRecords) {
- if (isActionMessage(messageRecord))
- {
+ if (isActionMessage(messageRecord)) {
actionMessage = true;
+ if (messageRecord.isInMemoryMessageRecord()) {
+ hasInMemory = true;
+ }
}
if (messageRecord.getBody().length() > 0) {
@@ -109,6 +118,7 @@ final class MenuState {
}
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText)
+ .shouldShowDeleteAction(!hasInMemory)
.build();
}
@@ -134,7 +144,8 @@ final class MenuState {
messageRecord.isIdentityDefault() ||
messageRecord.isProfileChange() ||
messageRecord.isGroupV1MigrationEvent() ||
- messageRecord.isFailedDecryptionType();
+ messageRecord.isFailedDecryptionType() ||
+ messageRecord.isInMemoryMessageRecord();
}
private final static class Builder {
@@ -145,6 +156,7 @@ final class MenuState {
private boolean saveAttachment;
private boolean resend;
private boolean copy;
+ private boolean delete;
@NonNull Builder shouldShowForwardAction(boolean forward) {
this.forward = forward;
@@ -176,6 +188,11 @@ final class MenuState {
return this;
}
+ @NonNull Builder shouldShowDeleteAction(boolean delete) {
+ this.delete = delete;
+ return this;
+ }
+
@NonNull
MenuState build() {
return new MenuState(this);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java
index 6347f43b1..411ba2438 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java
@@ -77,6 +77,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract int getMessageCountForThread(long threadId);
public abstract int getMessageCountForThread(long threadId, long beforeTime);
abstract int getMessageCountForThreadSummary(long threadId);
+ public abstract boolean hasMeaningfulMessage(long threadId);
public abstract Optional getNotification(long messageId);
public abstract Cursor getExpirationStartedMessages();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
index 210a9513c..cb77773cd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -620,6 +620,19 @@ public class MmsDatabase extends MessageDatabase {
return 0;
}
+ @Override
+ public boolean hasMeaningfulMessage(long threadId) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+
+ String[] cols = new String[] { "1" };
+ String query = THREAD_ID + " = ?";
+ String[] args = SqlUtil.buildArgs(threadId);
+
+ try (Cursor cursor = db.query(TABLE_NAME, cols, query, args, null, null, null, "1")) {
+ return cursor != null && cursor.moveToFirst();
+ }
+ }
+
@Override
public void addFailures(long messageId, List failure) {
try {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index 55f0308dc..1ae489e88 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -337,6 +337,15 @@ public class MmsSmsDatabase extends Database {
return count;
}
+ public boolean hasMeaningfulMessage(long threadId) {
+ if (threadId == -1) {
+ return false;
+ }
+
+ return DatabaseFactory.getSmsDatabase(context).hasMeaningfulMessage(threadId) ||
+ DatabaseFactory.getMmsDatabase(context).hasMeaningfulMessage(threadId);
+ }
+
public long getThreadForMessageId(long messageId) {
long id = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
index 030986519..db027d1d4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -233,14 +233,11 @@ public class SmsDatabase extends MessageDatabase {
@Override
public int getMessageCountForThreadSummary(long threadId) {
- SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ SqlUtil.Query query = buildMeaningfulMessagesQuery(threadId);
+ String[] cols = { "COUNT(*)" };
- String[] cols = { "COUNT(*)" };
- String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND TYPE != ?)";
- long type = Types.END_SESSION_BIT | Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT | Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
- String[] args = SqlUtil.buildArgs(threadId, type, Types.PROFILE_CHANGE_TYPE);
-
- try (Cursor cursor = db.query(TABLE_NAME, cols, query, args, null, null, null)) {
+ try (Cursor cursor = db.query(TABLE_NAME, cols, query.getWhere(), query.getWhereArgs(), null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int count = cursor.getInt(0);
if (count > 0) {
@@ -286,6 +283,22 @@ public class SmsDatabase extends MessageDatabase {
return 0;
}
+ @Override
+ public boolean hasMeaningfulMessage(long threadId) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ SqlUtil.Query query = buildMeaningfulMessagesQuery(threadId);
+
+ try (Cursor cursor = db.query(TABLE_NAME, new String[] { "1" }, query.getWhere(), query.getWhereArgs(), null, null, null, "1")) {
+ return cursor != null && cursor.moveToFirst();
+ }
+ }
+
+ private @NonNull SqlUtil.Query buildMeaningfulMessagesQuery(long threadId) {
+ String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND TYPE != ?)";
+ long type = Types.END_SESSION_BIT | Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT | Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
+ return SqlUtil.buildQuery(query, threadId, type, Types.PROFILE_CHANGE_TYPE);
+ }
+
@Override
public void markAsEndSession(long id) {
updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.END_SESSION_BIT);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
index a104b7eeb..1085a5f1b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -176,8 +176,12 @@ public class ThreadDatabase extends Database {
contentValues.put(MESSAGE_COUNT, 0);
- SQLiteDatabase db = databaseHelper.getWritableDatabase();
- return db.insert(TABLE_NAME, null, contentValues);
+ SQLiteDatabase db = databaseHelper.getWritableDatabase();
+ long result = db.insert(TABLE_NAME, null, contentValues);
+
+ Recipient.live(recipientId).refresh();
+
+ return result;
}
private void updateThread(long threadId, long count, String body, @Nullable Uri attachment,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java
index 9abfa601c..4952b1e1c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java
@@ -475,7 +475,7 @@ final class GroupsV2UpdateMessageProducer {
}
}
- private void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List updates) {
+ void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
if (change.hasNewTimer()) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java
index a3f993bf3..14db5ab3b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java
@@ -7,7 +7,9 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.util.ExpirationUtil;
import java.util.Collections;
@@ -16,6 +18,9 @@ import java.util.Collections;
*/
public class InMemoryMessageRecord extends MessageRecord {
+ private static final int NO_GROUPS_IN_COMMON_ID = -1;
+ private static final int UNIVERSAL_EXPIRE_TIMER_ID = -2;
+
private InMemoryMessageRecord(long id,
String body,
Recipient conversationRecipient,
@@ -78,7 +83,7 @@ public class InMemoryMessageRecord extends MessageRecord {
private final boolean isGroup;
public NoGroupsInCommon(long threadId, boolean isGroup) {
- super(-1, "", Recipient.UNKNOWN, threadId, 0);
+ super(NO_GROUPS_IN_COMMON_ID, "", Recipient.UNKNOWN, threadId, 0);
this.isGroup = isGroup;
}
@@ -108,4 +113,28 @@ public class InMemoryMessageRecord extends MessageRecord {
return R.string.ConversationUpdateItem_learn_more;
}
}
+
+ /**
+ * Show temporary update message about setting the disappearing messages timer upon first message
+ * send.
+ */
+ public static final class UniversalExpireTimerUpdate extends InMemoryMessageRecord {
+
+ public UniversalExpireTimerUpdate(long threadId) {
+ super(UNIVERSAL_EXPIRE_TIMER_ID, "", Recipient.UNKNOWN, threadId, 0);
+ }
+
+ @Override
+ public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context) {
+ String update = context.getString(R.string.ConversationUpdateItem_the_disappearing_message_time_will_be_set_to_s_when_you_message_them,
+ ExpirationUtil.getExpirationDisplayValue(context, SignalStore.settings().getUniversalExpireTimer()));
+
+ return UpdateDescription.staticDescription(update, R.drawable.ic_update_timer_16);
+ }
+
+ @Override
+ public boolean isUpdate() {
+ return true;
+ }
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
index 26b9c31fc..0aaf43400 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
@@ -240,11 +240,18 @@ public abstract class MessageRecord extends DisplayRecord {
if (decryptedGroupV2Context.hasChange() && (decryptedGroupV2Context.getGroupState().getRevision() != 0 || decryptedGroupV2Context.hasPreviousGroupState())) {
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getPreviousGroupState(), decryptedGroupV2Context.getChange()));
- } else if (selfCreatedGroup(decryptedGroupV2Context.getChange())) {
- return UpdateDescription.concatWithNewLines(Arrays.asList(updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange()),
- staticUpdateDescription(context.getString(R.string.MessageRecord_invite_friends_to_this_group), 0)));
} else {
- return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange());
+ List newGroupDescriptions = new ArrayList<>();
+ newGroupDescriptions.add(updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange()));
+
+ if (decryptedGroupV2Context.getChange().hasNewTimer()) {
+ updateMessageProducer.describeNewTimer(decryptedGroupV2Context.getChange(), newGroupDescriptions);
+ }
+
+ if (selfCreatedGroup(decryptedGroupV2Context.getChange())) {
+ newGroupDescriptions.add(staticUpdateDescription(context.getString(R.string.MessageRecord_invite_friends_to_this_group), 0));
+ }
+ return UpdateDescription.concatWithNewLines(newGroupDescriptions);
}
} catch (IOException e) {
Log.w(TAG, "GV2 Message update detail could not be read", e);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java
index 6c5ea4c07..7a2cf6eb2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java
@@ -36,11 +36,12 @@ public final class GroupManager {
private static final String TAG = Log.tag(GroupManager.class);
@WorkerThread
- public static @NonNull GroupActionResult createGroup(@NonNull Context context,
- @NonNull Set members,
- @Nullable byte[] avatar,
- @Nullable String name,
- boolean mms)
+ public static @NonNull GroupActionResult createGroup(@NonNull Context context,
+ @NonNull Set members,
+ @Nullable byte[] avatar,
+ @Nullable String name,
+ boolean mms,
+ int disappearingMessagesTimer)
throws GroupChangeBusyException, GroupChangeFailedException, IOException
{
boolean shouldAttemptToCreateV2 = !mms && !SignalStore.internalValues().gv2DoNotCreateGv2Groups();
@@ -49,7 +50,7 @@ public final class GroupManager {
if (shouldAttemptToCreateV2) {
try {
try (GroupManagerV2.GroupCreator groupCreator = new GroupManagerV2(context).create()) {
- return groupCreator.createGroup(memberIds, name, avatar);
+ return groupCreator.createGroup(memberIds, name, avatar, disappearingMessagesTimer);
}
} catch (MembershipNotSuitableForV2Exception e) {
Log.w(TAG, "Attempted to make a GV2, but membership was not suitable, falling back to GV1", e);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java
index c4e2fa506..4c39a0377 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java
@@ -244,23 +244,15 @@ final class GroupManagerV2 {
@WorkerThread
@NonNull GroupManager.GroupActionResult createGroup(@NonNull Collection members,
@Nullable String name,
- @Nullable byte[] avatar)
- throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
- {
- return createGroup(name, avatar, members);
- }
-
- @WorkerThread
- private @NonNull GroupManager.GroupActionResult createGroup(@Nullable String name,
- @Nullable byte[] avatar,
- @NonNull Collection members)
+ @Nullable byte[] avatar,
+ int disappearingMessagesTimer)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
{
GroupSecretParams groupSecretParams = GroupSecretParams.generate();
DecryptedGroup decryptedGroup;
try {
- decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, Member.Role.DEFAULT, 0);
+ decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, Member.Role.DEFAULT, disappearingMessagesTimer);
} catch (GroupAlreadyExistsException e) {
throw new GroupChangeFailedException(e);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupChangeFailureReason.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupChangeFailureReason.java
index 1cf6c919b..48245e98f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupChangeFailureReason.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/GroupChangeFailureReason.java
@@ -17,7 +17,7 @@ public enum GroupChangeFailureReason {
NETWORK,
OTHER;
- public static @NonNull GroupChangeFailureReason fromException(@NonNull Exception e) {
+ public static @NonNull GroupChangeFailureReason fromException(@NonNull Throwable e) {
if (e instanceof MembershipNotSuitableForV2Exception) return GroupChangeFailureReason.NOT_CAPABLE;
if (e instanceof IOException) return GroupChangeFailureReason.NETWORK;
if (e instanceof GroupNotAMemberException) return GroupChangeFailureReason.NOT_A_MEMBER;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java
index 36761af29..1e0cd5f8f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java
@@ -12,6 +12,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
+import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -30,8 +31,10 @@ import com.dd.CircularProgressButton;
import org.signal.core.util.EditTextUtil;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.settings.app.privacy.expire.ExpireTimerSettingsFragment;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
import org.thoughtcrime.securesms.groups.ui.creategroup.dialogs.NonGv2MemberDialog;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
import org.thoughtcrime.securesms.mediasend.Media;
@@ -40,7 +43,9 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
import org.thoughtcrime.securesms.util.BitmapUtil;
+import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
@@ -54,6 +59,7 @@ public class AddGroupDetailsFragment extends LoggingFragment {
private static final int AVATAR_PLACEHOLDER_INSET_DP = 18;
private static final short REQUEST_CODE_AVATAR = 27621;
+ private static final short REQUEST_DISAPPEARING_TIMER = 28621;
private CircularProgressButton create;
private Callback callback;
@@ -61,6 +67,7 @@ public class AddGroupDetailsFragment extends LoggingFragment {
private Drawable avatarPlaceholder;
private EditText name;
private Toolbar toolbar;
+ private View disappearingMessagesRow;
@Override
public void onAttach(@NonNull Context context) {
@@ -83,17 +90,19 @@ public class AddGroupDetailsFragment extends LoggingFragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- create = view.findViewById(R.id.create);
- name = view.findViewById(R.id.name);
- toolbar = view.findViewById(R.id.toolbar);
+ create = view.findViewById(R.id.create);
+ name = view.findViewById(R.id.name);
+ toolbar = view.findViewById(R.id.toolbar);
+ disappearingMessagesRow = view.findViewById(R.id.group_disappearing_messages_row);
setCreateEnabled(false, false);
- GroupMemberListView members = view.findViewById(R.id.member_list);
- ImageView avatar = view.findViewById(R.id.group_avatar);
- View mmsWarning = view.findViewById(R.id.mms_warning);
- LearnMoreTextView gv2Warning = view.findViewById(R.id.gv2_warning);
- View addLater = view.findViewById(R.id.add_later);
+ GroupMemberListView members = view.findViewById(R.id.member_list);
+ ImageView avatar = view.findViewById(R.id.group_avatar);
+ View mmsWarning = view.findViewById(R.id.mms_warning);
+ LearnMoreTextView gv2Warning = view.findViewById(R.id.gv2_warning);
+ View addLater = view.findViewById(R.id.add_later);
+ TextView disappearingMessageValue = view.findViewById(R.id.group_disappearing_messages_value);
avatarPlaceholder = VectorDrawableCompat.create(getResources(), R.drawable.ic_camera_outline_32_ultramarine, requireActivity().getTheme());
@@ -115,6 +124,7 @@ public class AddGroupDetailsFragment extends LoggingFragment {
});
viewModel.getCanSubmitForm().observe(getViewLifecycleOwner(), isFormValid -> setCreateEnabled(isFormValid, true));
viewModel.getIsMms().observe(getViewLifecycleOwner(), isMms -> {
+ disappearingMessagesRow.setVisibility(isMms ? View.GONE : View.VISIBLE);
mmsWarning.setVisibility(isMms ? View.VISIBLE : View.GONE);
name.setHint(isMms ? R.string.AddGroupDetailsFragment__group_name_optional : R.string.AddGroupDetailsFragment__group_name_required);
toolbar.setTitle(isMms ? R.string.AddGroupDetailsFragment__create_group : R.string.AddGroupDetailsFragment__name_this_group);
@@ -143,6 +153,11 @@ public class AddGroupDetailsFragment extends LoggingFragment {
}
});
+ viewModel.getDisappearingMessagesTimer().observe(getViewLifecycleOwner(), timer -> disappearingMessageValue.setText(ExpirationUtil.getExpirationDisplayValue(requireContext(), timer)));
+ disappearingMessagesRow.setOnClickListener(v -> {
+ startActivityForResult(RecipientDisappearingMessagesActivity.forCreateGroup(requireContext(), viewModel.getDisappearingMessagesTimer().getValue()), REQUEST_DISAPPEARING_TIMER);
+ });
+
name.requestFocus();
}
@@ -175,6 +190,8 @@ public class AddGroupDetailsFragment extends LoggingFragment {
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
+ } else if (requestCode == REQUEST_DISAPPEARING_TIMER && resultCode == Activity.RESULT_OK && data != null) {
+ viewModel.setDisappearingMessageTimer(data.getIntExtra(ExpireTimerSettingsFragment.FOR_RESULT_VALUE, SignalStore.settings().getUniversalExpireTimer()));
} else {
super.onActivityResult(requestCode, resultCode, data);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java
index e36d89771..62b322a1a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsRepository.java
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.groups.GroupChangeException;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupsV2CapabilityChecker;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -48,17 +49,24 @@ final class AddGroupDetailsRepository {
});
}
- void createGroup(@NonNull Set members,
- @Nullable byte[] avatar,
- @Nullable String name,
- boolean mms,
+ void createGroup(@NonNull Set members,
+ @Nullable byte[] avatar,
+ @Nullable String name,
+ boolean mms,
+ @Nullable Integer disappearingMessagesTimer,
Consumer resultConsumer)
{
SignalExecutors.BOUNDED.execute(() -> {
Set recipients = new HashSet<>(Stream.of(members).map(Recipient::resolved).toList());
try {
- GroupManager.GroupActionResult result = GroupManager.createGroup(context, recipients, avatar, name, mms);
+ GroupManager.GroupActionResult result = GroupManager.createGroup(context,
+ recipients,
+ avatar,
+ name,
+ mms,
+ disappearingMessagesTimer != null ? disappearingMessagesTimer
+ : SignalStore.settings().getUniversalExpireTimer());
resultConsumer.accept(GroupCreateResult.success(result));
} catch (GroupChangeBusyException e) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java
index d225781d3..2b1a792e1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java
@@ -32,10 +32,11 @@ import java.util.Set;
public final class AddGroupDetailsViewModel extends ViewModel {
private final LiveData> members;
- private final DefaultValueLiveData> deleted = new DefaultValueLiveData<>(new HashSet<>());
- private final MutableLiveData name = new MutableLiveData<>("");
- private final MutableLiveData avatar = new MutableLiveData<>();
- private final SingleLiveEvent groupCreateResult = new SingleLiveEvent<>();
+ private final DefaultValueLiveData> deleted = new DefaultValueLiveData<>(new HashSet<>());
+ private final MutableLiveData name = new MutableLiveData<>("");
+ private final MutableLiveData avatar = new MutableLiveData<>();
+ private final SingleLiveEvent groupCreateResult = new SingleLiveEvent<>();
+ private final MutableLiveData disappearingMessagesTimer = new MutableLiveData<>(SignalStore.settings().getUniversalExpireTimer());
private final LiveData isMms;
private final LiveData canSubmitForm;
private final AddGroupDetailsRepository repository;
@@ -47,12 +48,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
this.repository = repository;
MutableLiveData> initialMembers = new MutableLiveData<>();
+ LiveData isValidName = Transformations.map(name, name -> !TextUtils.isEmpty(name));
- LiveData isValidName = Transformations.map(name, name -> !TextUtils.isEmpty(name));
-
- members = LiveDataUtil.combineLatest(initialMembers, deleted, AddGroupDetailsViewModel::filterDeletedMembers);
-
- isMms = Transformations.map(members, AddGroupDetailsViewModel::isAnyForcedSms);
+ members = LiveDataUtil.combineLatest(initialMembers, deleted, AddGroupDetailsViewModel::filterDeletedMembers);
+ isMms = Transformations.map(members, AddGroupDetailsViewModel::isAnyForcedSms);
LiveData> membersToCheckGv2CapabilityOf = LiveDataUtil.combineLatest(isMms, members, (forcedMms, memberList) -> {
if (SignalStore.internalValues().gv2DoNotCreateGv2Groups() || forcedMms) {
@@ -94,6 +93,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
return nonGv2CapableMembers;
}
+ @NonNull LiveData getDisappearingMessagesTimer() {
+ return disappearingMessagesTimer;
+ }
+
void setAvatar(@Nullable byte[] avatar) {
this.avatar.setValue(avatar);
}
@@ -107,18 +110,19 @@ public final class AddGroupDetailsViewModel extends ViewModel {
}
void delete(@NonNull RecipientId recipientId) {
- Set deleted = this.deleted.getValue();
+ Set deleted = this.deleted.getValue();
deleted.add(recipientId);
this.deleted.setValue(deleted);
}
void create() {
- List members = Objects.requireNonNull(this.members.getValue());
- Set memberIds = Stream.of(members).map(member -> member.getMember().getId()).collect(Collectors.toSet());
- byte[] avatarBytes = avatar.getValue();
- boolean isGroupMms = isMms.getValue() == Boolean.TRUE;
- String groupName = name.getValue();
+ List members = Objects.requireNonNull(this.members.getValue());
+ Set memberIds = Stream.of(members).map(member -> member.getMember().getId()).collect(Collectors.toSet());
+ byte[] avatarBytes = avatar.getValue();
+ boolean isGroupMms = isMms.getValue() == Boolean.TRUE;
+ String groupName = name.getValue();
+ Integer disappearingTimer = disappearingMessagesTimer.getValue();
if (!isGroupMms && TextUtils.isEmpty(groupName)) {
groupCreateResult.postValue(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_INVALID_NAME));
@@ -129,6 +133,7 @@ public final class AddGroupDetailsViewModel extends ViewModel {
avatarBytes,
groupName,
isGroupMms,
+ disappearingTimer,
groupCreateResult::postValue);
}
@@ -143,6 +148,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
.anyMatch(member -> !member.getMember().isRegistered());
}
+ public void setDisappearingMessageTimer(int timer) {
+ disappearingMessagesTimer.setValue(timer);
+ }
+
static final class Factory implements ViewModelProvider.Factory {
private final Collection recipientIds;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java
index 238908fad..0c4cc6480 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java
@@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
+import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroupLinkDialogFragment;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
@@ -280,7 +281,12 @@ public class ManageGroupFragment extends LoggingFragment {
viewModel.getDisappearingMessageTimer().observe(getViewLifecycleOwner(), string -> disappearingMessages.setText(string));
- disappearingMessagesRow.setOnClickListener(v -> viewModel.handleExpirationSelection());
+ disappearingMessagesRow.setOnClickListener(v -> {
+ Recipient recipient = viewModel.getGroupRecipient().getValue();
+ if (recipient != null) {
+ startActivity(RecipientDisappearingMessagesActivity.forRecipient(requireContext(), recipient.getId()));
+ }
+ });
blockGroup.setOnClickListener(v -> viewModel.blockAndLeave(requireActivity()));
unblockGroup.setOnClickListener(v -> viewModel.unblock(requireActivity()));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java
index d0fd5e03a..9212497a8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java
@@ -79,17 +79,6 @@ final class ManageGroupRepository {
return new GroupStateResult(threadId, groupRecipient);
}
- void setExpiration(@NonNull GroupId groupId, int newExpirationTime, @NonNull GroupChangeErrorCallback error) {
- SignalExecutors.UNBOUNDED.execute(() -> {
- try {
- GroupManager.updateGroupTimer(context, groupId.requirePush(), newExpirationTime);
- } catch (GroupChangeException | IOException e) {
- Log.w(TAG, e);
- error.onError(GroupChangeFailureReason.fromException(e));
- }
- });
- }
-
void applyMembershipRightsChange(@NonNull GroupId groupId, @NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) {
SignalExecutors.UNBOUNDED.execute(() -> {
try {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java
index d5182bb02..7926e17f2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java
@@ -257,14 +257,6 @@ public class ManageGroupViewModel extends ViewModel {
return groupInfoMessage;
}
- void handleExpirationSelection() {
- manageGroupRepository.getRecipient(getGroupId(),
- groupRecipient ->
- ExpirationDialog.show(context,
- groupRecipient.getExpireMessages(),
- expirationTime -> manageGroupRepository.setExpiration(getGroupId(), expirationTime, this::showErrorToast)));
- }
-
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights) {
manageGroupRepository.applyMembershipRightsChange(getGroupId(), newRights, this::showErrorToast);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
index b5607cc1c..37f5f4016 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
@@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.RetrieveProfileJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration;
import org.thoughtcrime.securesms.migrations.AccountRecordMigrationJob;
+import org.thoughtcrime.securesms.migrations.ApplyUnknownFieldsToSelfMigrationJob;
import org.thoughtcrime.securesms.migrations.AttributesMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
@@ -160,6 +161,7 @@ public final class JobManagerFactories {
// Migrations
put(AccountRecordMigrationJob.KEY, new AccountRecordMigrationJob.Factory());
+ put(ApplyUnknownFieldsToSelfMigrationJob.KEY, new ApplyUnknownFieldsToSelfMigrationJob.Factory());
put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory());
put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory());
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java
index d1fbb22b3..615a583b1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java
@@ -9,15 +9,15 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
+import org.signal.core.util.concurrent.SignalExecutors;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
-import org.thoughtcrime.securesms.util.SingleLiveEvent;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.signal.core.util.concurrent.SignalExecutors;
-import org.signal.core.util.logging.Log;
-import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
+import org.thoughtcrime.securesms.util.SingleLiveEvent;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode;
import java.util.Arrays;
@@ -41,31 +41,31 @@ public final class SettingsValues extends SignalStoreValues {
public static final String THREAD_TRIM_LENGTH = "pref_trim_length";
public static final String THREAD_TRIM_ENABLED = "pref_trim_threads";
- public static final String THEME = "settings.theme";
- public static final String MESSAGE_FONT_SIZE = "settings.message.font.size";
- public static final String LANGUAGE = "settings.language";
- public static final String PREFER_SYSTEM_EMOJI = "settings.use.system.emoji";
- public static final String ENTER_KEY_SENDS = "settings.enter.key.sends";
- public static final String BACKUPS_ENABLED = "settings.backups.enabled";
- public static final String SMS_DELIVERY_REPORTS_ENABLED = "settings.sms.delivery.reports.enabled";
- public static final String WIFI_CALLING_COMPATIBILITY_MODE_ENABLED = "settings.wifi.calling.compatibility.mode.enabled";
- public static final String MESSAGE_NOTIFICATIONS_ENABLED = "settings.message.notifications.enabled";
- public static final String MESSAGE_NOTIFICATION_SOUND = "settings.message.notifications.sound";
- public static final String MESSAGE_VIBRATE_ENABLED = "settings.message.vibrate.enabled";
- public static final String MESSAGE_LED_COLOR = "settings.message.led.color";
- public static final String MESSAGE_LED_BLINK_PATTERN = "settings.message.led.blink";
- public static final String MESSAGE_IN_CHAT_SOUNDS_ENABLED = "settings.message.in.chats.sounds.enabled";
- public static final String MESSAGE_REPEAT_ALERTS = "settings.message.repeat.alerts";
- public static final String MESSAGE_NOTIFICATION_PRIVACY = "settings.message.notification.privacy";
- public static final String CALL_NOTIFICATIONS_ENABLED = "settings.call.notifications.enabled";
- public static final String CALL_RINGTONE = "settings.call.ringtone";
- public static final String CALL_VIBRATE_ENABLED = "settings.call.vibrate.enabled";
- public static final String NOTIFY_WHEN_CONTACT_JOINS_SIGNAL = "settings.notify.when.contact.joins.signal";
+ public static final String THEME = "settings.theme";
+ public static final String MESSAGE_FONT_SIZE = "settings.message.font.size";
+ public static final String LANGUAGE = "settings.language";
+ public static final String PREFER_SYSTEM_EMOJI = "settings.use.system.emoji";
+ public static final String ENTER_KEY_SENDS = "settings.enter.key.sends";
+ public static final String BACKUPS_ENABLED = "settings.backups.enabled";
+ public static final String SMS_DELIVERY_REPORTS_ENABLED = "settings.sms.delivery.reports.enabled";
+ public static final String WIFI_CALLING_COMPATIBILITY_MODE_ENABLED = "settings.wifi.calling.compatibility.mode.enabled";
+ public static final String MESSAGE_NOTIFICATIONS_ENABLED = "settings.message.notifications.enabled";
+ public static final String MESSAGE_NOTIFICATION_SOUND = "settings.message.notifications.sound";
+ public static final String MESSAGE_VIBRATE_ENABLED = "settings.message.vibrate.enabled";
+ public static final String MESSAGE_LED_COLOR = "settings.message.led.color";
+ public static final String MESSAGE_LED_BLINK_PATTERN = "settings.message.led.blink";
+ public static final String MESSAGE_IN_CHAT_SOUNDS_ENABLED = "settings.message.in.chats.sounds.enabled";
+ public static final String MESSAGE_REPEAT_ALERTS = "settings.message.repeat.alerts";
+ public static final String MESSAGE_NOTIFICATION_PRIVACY = "settings.message.notification.privacy";
+ public static final String CALL_NOTIFICATIONS_ENABLED = "settings.call.notifications.enabled";
+ public static final String CALL_RINGTONE = "settings.call.ringtone";
+ public static final String CALL_VIBRATE_ENABLED = "settings.call.vibrate.enabled";
+ public static final String NOTIFY_WHEN_CONTACT_JOINS_SIGNAL = "settings.notify.when.contact.joins.signal";
+ private static final String DEFAULT_SMS = "settings.default_sms";
+ private static final String UNIVERSAL_EXPIRE_TIMER = "settings.universal.expire.timer";
private final SingleLiveEvent onConfigurationSettingChanged = new SingleLiveEvent<>();
- private static final String DEFAULT_SMS = "settings.default_sms";
-
SettingsValues(@NonNull KeyValueStore store) {
super(store);
}
@@ -104,7 +104,8 @@ public final class SettingsValues extends SignalStoreValues {
CALL_NOTIFICATIONS_ENABLED,
CALL_RINGTONE,
CALL_VIBRATE_ENABLED,
- NOTIFY_WHEN_CONTACT_JOINS_SIGNAL);
+ NOTIFY_WHEN_CONTACT_JOINS_SIGNAL,
+ UNIVERSAL_EXPIRE_TIMER);
}
public @NonNull LiveData getOnConfigurationSettingChanged() {
@@ -370,6 +371,18 @@ public final class SettingsValues extends SignalStoreValues {
}
}
+ public void setUniversalExpireTimer(int seconds) {
+ putInteger(UNIVERSAL_EXPIRE_TIMER, seconds);
+ SignalExecutors.BOUNDED.execute(() -> {
+ DatabaseFactory.getRecipientDatabase(ApplicationDependencies.getApplication()).markNeedsSync(Recipient.self().getId());
+ StorageSyncHelper.scheduleSyncForDataChange();
+ });
+ }
+
+ public int getUniversalExpireTimer() {
+ return getInteger(UNIVERSAL_EXPIRE_TIMER, 0);
+ }
+
private @Nullable Uri getUri(@NonNull String key) {
String uri = getString(key, "");
diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java
index 81458f45a..7c62a4180 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java
@@ -40,7 +40,7 @@ public class ApplicationMigrations {
private static final int LEGACY_CANONICAL_VERSION = 455;
- public static final int CURRENT_VERSION = 33;
+ public static final int CURRENT_VERSION = 34;
private static final class Version {
static final int LEGACY = 1;
@@ -75,6 +75,7 @@ public class ApplicationMigrations {
static final int MUTE_SYNC = 31;
static final int PROFILE_SHARING_UPDATE = 32;
static final int SMS_STORAGE_SYNC = 33;
+ static final int APPLY_UNIVERSAL_EXPIRE = 34;
}
/**
@@ -317,6 +318,10 @@ public class ApplicationMigrations {
jobs.put(Version.SMS_STORAGE_SYNC, new AccountRecordMigrationJob());
}
+ if (lastSeenVersion < Version.APPLY_UNIVERSAL_EXPIRE) {
+ jobs.put(Version.SMS_STORAGE_SYNC, new ApplyUnknownFieldsToSelfMigrationJob());
+ }
+
return jobs;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplyUnknownFieldsToSelfMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplyUnknownFieldsToSelfMigrationJob.java
new file mode 100644
index 000000000..96d705bcc
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplyUnknownFieldsToSelfMigrationJob.java
@@ -0,0 +1,86 @@
+package org.thoughtcrime.securesms.migrations;
+
+import androidx.annotation.NonNull;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.RecipientDatabase;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.jobmanager.Data;
+import org.thoughtcrime.securesms.jobmanager.Job;
+import org.thoughtcrime.securesms.jobs.StorageSyncJob;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.storage.StorageSyncHelper;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
+import org.whispersystems.signalservice.api.storage.StorageId;
+import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
+
+/**
+ * Check for unknown fields stored on self and attempt to apply them.
+ */
+public class ApplyUnknownFieldsToSelfMigrationJob extends MigrationJob {
+
+ private static final String TAG = Log.tag(ApplyUnknownFieldsToSelfMigrationJob.class);
+
+ public static final String KEY = "ApplyUnknownFieldsToSelfMigrationJob";
+
+ ApplyUnknownFieldsToSelfMigrationJob() {
+ this(new Parameters.Builder().build());
+ }
+
+ private ApplyUnknownFieldsToSelfMigrationJob(@NonNull Parameters parameters) {
+ super(parameters);
+ }
+
+ @Override
+ public boolean isUiBlocking() {
+ return false;
+ }
+
+ @Override
+ public @NonNull String getFactoryKey() {
+ return KEY;
+ }
+
+ @Override
+ public void performMigration() {
+ if (!TextSecurePreferences.isPushRegistered(context) || TextSecurePreferences.getLocalUuid(context) == null) {
+ Log.w(TAG, "Not registered!");
+ return;
+ }
+
+ Recipient self = Recipient.self();
+ RecipientDatabase.RecipientSettings settings = DatabaseFactory.getRecipientDatabase(context).getRecipientSettingsForSync(self.getId());
+
+ if (settings == null || settings.getSyncExtras().getStorageProto() == null) {
+ Log.d(TAG, "No unknowns to apply");
+ return;
+ }
+
+ try {
+ StorageId storageId = StorageId.forAccount(self.getStorageServiceId());
+ AccountRecord accountRecord = AccountRecord.parseFrom(settings.getSyncExtras().getStorageProto());
+ SignalAccountRecord signalAccountRecord = new SignalAccountRecord(storageId, accountRecord);
+
+ Log.d(TAG, "Applying potentially now known unknowns");
+ StorageSyncHelper.applyAccountStorageSyncUpdates(context, self, signalAccountRecord, false);
+ } catch (InvalidProtocolBufferException e) {
+ Log.w(TAG, e);
+ }
+ }
+
+ @Override
+ boolean shouldRetry(@NonNull Exception e) {
+ return false;
+ }
+
+ public static class Factory implements Job.Factory {
+ @Override
+ public @NonNull ApplyUnknownFieldsToSelfMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
+ return new ApplyUnknownFieldsToSelfMigrationJob(parameters);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java
index 505fd503a..0a70ff7e4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingMediaMessage.java
@@ -78,6 +78,23 @@ public class OutgoingMediaMessage {
contacts, linkPreviews, mentions, new LinkedList<>(), new LinkedList<>());
}
+ public OutgoingMediaMessage(OutgoingMediaMessage that, long expiresIn) {
+ this(that.getRecipient(),
+ that.body,
+ that.attachments,
+ that.sentTimeMillis,
+ that.subscriptionId,
+ expiresIn,
+ that.viewOnce,
+ that.distributionType,
+ that.outgoingQuote,
+ that.contacts,
+ that.linkPreviews,
+ that.mentions,
+ that.networkFailures,
+ that.identityKeyMismatches);
+ }
+
public OutgoingMediaMessage(OutgoingMediaMessage that) {
this.recipient = that.getRecipient();
this.body = that.body;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java
index 5ac8926ca..419d7c5e8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java
@@ -23,6 +23,8 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
+import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
+import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -285,6 +287,26 @@ public class RecipientUtil {
}
}
+ /**
+ * Checks if a universal timer is set and if the thread should have it set on it. Attempts to abort quickly and perform
+ * minimal database access.
+ */
+ @WorkerThread
+ public static boolean setAndSendUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, long threadId) {
+ int defaultTimer = SignalStore.settings().getUniversalExpireTimer();
+ if (defaultTimer == 0 || recipient.isGroup() || recipient.getExpireMessages() != 0) {
+ return false;
+ }
+
+ if (threadId == -1 || !DatabaseFactory.getMmsSmsDatabase(context).hasMeaningfulMessage(threadId)) {
+ DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient.getId(), defaultTimer);
+ OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(recipient, System.currentTimeMillis(), defaultTimer * 1000L);
+ MessageSender.send(context, outgoingMessage, DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient), false, null);
+ return true;
+ }
+ return false;
+ }
+
@WorkerThread
private static boolean isMessageRequestAccepted(@NonNull Context context, long threadId, @NonNull Recipient threadRecipient) {
return threadRecipient.isSelf() ||
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/disappearingmessages/RecipientDisappearingMessagesActivity.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/disappearingmessages/RecipientDisappearingMessagesActivity.java
new file mode 100644
index 000000000..90f003bac
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/disappearingmessages/RecipientDisappearingMessagesActivity.java
@@ -0,0 +1,38 @@
+package org.thoughtcrime.securesms.recipients.ui.disappearingmessages;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity;
+import org.thoughtcrime.securesms.components.settings.app.privacy.expire.ExpireTimerSettingsFragmentArgs;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+
+/**
+ * For select a expire timer for a recipient (individual or group).
+ */
+public final class RecipientDisappearingMessagesActivity extends DSLSettingsActivity {
+
+ public static @NonNull Intent forRecipient(@NonNull Context context, @NonNull RecipientId recipientId) {
+ Intent intent = new Intent(context, RecipientDisappearingMessagesActivity.class);
+ intent.putExtra(DSLSettingsActivity.ARG_NAV_GRAPH, R.navigation.app_settings_expire_timer)
+ .putExtra(DSLSettingsActivity.ARG_START_BUNDLE, new ExpireTimerSettingsFragmentArgs.Builder().setRecipientId(recipientId).build().toBundle());
+
+ return intent;
+ }
+
+ public static @NonNull Intent forCreateGroup(@NonNull Context context, @Nullable Integer initialValue) {
+ Intent intent = new Intent(context, RecipientDisappearingMessagesActivity.class);
+ intent.putExtra(DSLSettingsActivity.ARG_NAV_GRAPH, R.navigation.app_settings_expire_timer)
+ .putExtra(DSLSettingsActivity.ARG_START_BUNDLE, new ExpireTimerSettingsFragmentArgs.Builder().setForResultMode(true)
+ .setInitialValue(initialValue)
+ .build()
+ .toBundle());
+
+ return intent;
+ }
+
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java
index b5b844890..e532ccae4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java
@@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
@@ -239,7 +240,7 @@ public class ManageRecipientFragment extends LoggingFragment {
internalDetails.setVisibility(View.GONE);
}
- disappearingMessagesRow.setOnClickListener(v -> viewModel.handleExpirationSelection(requireContext()));
+ disappearingMessagesRow.setOnClickListener(v -> startActivity(RecipientDisappearingMessagesActivity.forRecipient(requireContext(), recipientId)));
block.setOnClickListener(v -> viewModel.onBlockClicked(requireActivity()));
unblock.setOnClickListener(v -> viewModel.onUnblockClicked(requireActivity()));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java
index c031f1382..a80b5e765 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java
@@ -19,10 +19,8 @@ import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
-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;
import java.util.ArrayList;
@@ -62,14 +60,6 @@ final class ManageRecipientRepository {
.orNull()));
}
- void setExpiration(int newExpirationTime) {
- SignalExecutors.BOUNDED.execute(() -> {
- DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipientId, newExpirationTime);
- OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L);
- MessageSender.send(context, outgoingMessage, getThreadId(), false, null);
- });
- }
-
void getGroupMembership(@NonNull Consumer> onComplete) {
SignalExecutors.BOUNDED.execute(() -> {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java
index 73d105fee..f16e480c8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java
@@ -187,13 +187,6 @@ public final class ManageRecipientViewModel extends ViewModel {
return canUnblock;
}
- void handleExpirationSelection(@NonNull Context context) {
- withRecipient(recipient ->
- ExpirationDialog.show(context,
- recipient.getExpireMessages(),
- manageRecipientRepository::setExpiration));
- }
-
void setMuteUntil(long muteUntil) {
manageRecipientRepository.setMuteUntil(muteUntil);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java
index 104da7728..fde99a367 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java
@@ -15,6 +15,8 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.OfferMetadata;
@@ -76,6 +78,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, remotePeer);
webRtcInteractor.setWantsBluetoothConnection(true);
+ RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, Recipient.resolved(remotePeer.getId()), DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(remotePeer.getId()));
DatabaseFactory.getSmsDatabase(context).insertOutgoingCall(remotePeer.getId(), currentState.getCallSetupState().isEnableVideoOnCreate());
webRtcInteractor.retrieveTurnServers(remotePeer);
@@ -97,6 +100,8 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
Integer destinationDeviceId = broadcast ? null : callMetadata.getRemoteDevice();
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOffer(offerMessage, true, destinationDeviceId);
+ Recipient callRecipient = currentState.getCallInfoState().getCallRecipient();
+ RecipientUtil.shareProfileIfFirstSecureMessage(context, callRecipient);
webRtcInteractor.sendCallMessage(callMetadata.getRemotePeer(), callMessage);
return currentState;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java
index 2d1c6df90..80d27587e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java
@@ -28,7 +28,6 @@ import com.annimon.stream.Stream;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.logging.Log;
-import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
@@ -62,12 +61,14 @@ import org.thoughtcrime.securesms.jobs.ReactionSendJob;
import org.thoughtcrime.securesms.jobs.RemoteDeleteSendJob;
import org.thoughtcrime.securesms.jobs.ResumableUploadSpecJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
+import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.ParcelUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -106,7 +107,11 @@ public class MessageSender {
boolean keyExchange = message.isKeyExchange();
long allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateValidThreadId(recipient, threadId);
- long messageId = database.insertMessageOutbox(allocatedThreadId, message, forceSms, System.currentTimeMillis(), insertListener);
+ long messageId = database.insertMessageOutbox(allocatedThreadId,
+ applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId),
+ forceSms,
+ System.currentTimeMillis(),
+ insertListener);
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
onMessageSent();
@@ -127,7 +132,7 @@ public class MessageSender {
long allocatedThreadId = threadDatabase.getOrCreateValidThreadId(message.getRecipient(), threadId, message.getDistributionType());
Recipient recipient = message.getRecipient();
- long messageId = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
+ long messageId = database.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId), allocatedThreadId, forceSms, insertListener);
sendMediaMessage(context, recipient, forceSms, messageId, Collections.emptyList());
onMessageSent();
@@ -163,7 +168,10 @@ public class MessageSender {
}
Recipient recipient = message.getRecipient();
- long messageId = mmsDatabase.insertMessageOutbox(message, allocatedThreadId, false, insertListener);
+ long messageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId),
+ allocatedThreadId,
+ false,
+ insertListener);
List attachmentIds = Stream.of(preUploadResults).map(PreUploadResult::getAttachmentId).toList();
List jobIds = Stream.of(preUploadResults).map(PreUploadResult::getJobIds).flatMap(Stream::of).toList();
@@ -198,7 +206,10 @@ public class MessageSender {
try {
OutgoingSecureMediaMessage primaryMessage = messages.get(0);
long primaryThreadId = threadDatabase.getThreadIdFor(primaryMessage.getRecipient(), primaryMessage.getDistributionType());
- long primaryMessageId = mmsDatabase.insertMessageOutbox(primaryMessage, primaryThreadId, false, null);
+ long primaryMessageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, primaryMessage.getRecipient(), primaryMessage, primaryThreadId),
+ primaryThreadId,
+ false,
+ null);
attachmentDatabase.updateMessageId(preUploadAttachmentIds, primaryMessageId);
messageIds.add(primaryMessageId);
@@ -216,7 +227,10 @@ public class MessageSender {
for (OutgoingSecureMediaMessage secondaryMessage : secondaryMessages) {
long allocatedThreadId = threadDatabase.getThreadIdFor(secondaryMessage.getRecipient(), secondaryMessage.getDistributionType());
- long messageId = mmsDatabase.insertMessageOutbox(secondaryMessage, allocatedThreadId, false, null);
+ long messageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, secondaryMessage.getRecipient(), secondaryMessage, allocatedThreadId),
+ allocatedThreadId,
+ false,
+ null);
List attachmentIds = new ArrayList<>(preUploadAttachmentIds.size());
for (int i = 0; i < preUploadAttachments.size(); i++) {
@@ -355,6 +369,20 @@ public class MessageSender {
EventBus.getDefault().postSticky(MessageSentEvent.INSTANCE);
}
+ private static @NonNull OutgoingTextMessage applyUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, @NonNull OutgoingTextMessage outgoingTextMessage, long threadId) {
+ if (outgoingTextMessage.getExpiresIn() == 0 && RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, recipient, threadId)) {
+ return new OutgoingTextMessage(outgoingTextMessage, SignalStore.settings().getUniversalExpireTimer() * 1000L);
+ }
+ return outgoingTextMessage;
+ }
+
+ private static @NonNull OutgoingMediaMessage applyUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, @NonNull OutgoingMediaMessage outgoingMediaMessage, long threadId) {
+ if (!outgoingMediaMessage.isExpirationUpdate() && outgoingMediaMessage.getExpiresIn() == 0 && RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, recipient, threadId)) {
+ return new OutgoingMediaMessage(outgoingMediaMessage, SignalStore.settings().getUniversalExpireTimer() * 1000L);
+ }
+ return outgoingMediaMessage;
+ }
+
private static void sendMediaMessage(Context context, Recipient recipient, boolean forceSms, long messageId, @NonNull Collection uploadJobIds)
{
if (isLocalSelfSend(context, recipient, forceSms)) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java
index e74cb8ff9..d22f12f65 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/sms/OutgoingTextMessage.java
@@ -28,6 +28,10 @@ public class OutgoingTextMessage {
this.message = body;
}
+ public OutgoingTextMessage(OutgoingTextMessage base, long expiresIn) {
+ this(base.getRecipient(), base.getMessageBody(), expiresIn, base.getSubscriptionId());
+ }
+
public long getExpiresIn() {
return expiresIn;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java
index 9c3f02a47..705abc453 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java
@@ -97,9 +97,10 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor pinnedConversations = remote.getPinnedConversations();
AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode = remote.getPhoneNumberSharingMode();
boolean preferContactAvatars = remote.isPreferContactAvatars();
+ int universalExpireTimer = remote.getUniversalExpireTimer();
boolean primarySendsSms = local.isPrimarySendsSms();
- boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, primarySendsSms);
- boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, primarySendsSms);
+ boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms);
+ boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms);
if (matchesRemote) {
return remote;
@@ -124,6 +125,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor pinnedConversations,
boolean preferContactAvatars,
SignalAccountRecord.Payments payments,
+ int universalExpireTimer,
boolean primarySendsSms)
{
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
@@ -178,6 +181,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor LiveData.distinctUntilChanged(selector: (T) -> R): LiveData {
+ return LiveDataUtil.distinctUntilChanged(this, selector)
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/ProcessState.kt b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/ProcessState.kt
new file mode 100644
index 000000000..ff54a77f1
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/ProcessState.kt
@@ -0,0 +1,22 @@
+package org.thoughtcrime.securesms.util.livedata
+
+/**
+ * Provide a general representation of a discrete process. States are idle,
+ * working, success, and failure.
+ */
+sealed class ProcessState {
+ class Idle : ProcessState()
+ class Working : ProcessState()
+ data class Success(val result: T) : ProcessState()
+ data class Failure(val throwable: Throwable?) : ProcessState()
+
+ companion object {
+ fun fromResult(result: Result): ProcessState {
+ return if (result.isSuccess) {
+ Success(result.getOrThrow())
+ } else {
+ Failure(result.exceptionOrNull())
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/layout/add_group_details_fragment.xml b/app/src/main/res/layout/add_group_details_fragment.xml
index 00c1b21af..8f579766b 100644
--- a/app/src/main/res/layout/add_group_details_fragment.xml
+++ b/app/src/main/res/layout/add_group_details_fragment.xml
@@ -39,6 +39,42 @@
app:layout_constraintStart_toEndOf="@id/group_avatar"
app:layout_constraintTop_toTopOf="@id/group_avatar" />
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/custom_expire_timer_selector_view.xml b/app/src/main/res/layout/custom_expire_timer_selector_view.xml
new file mode 100644
index 000000000..63aaa4e98
--- /dev/null
+++ b/app/src/main/res/layout/custom_expire_timer_selector_view.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dsl_radio_preference_item.xml b/app/src/main/res/layout/dsl_radio_preference_item.xml
new file mode 100644
index 000000000..924e03379
--- /dev/null
+++ b/app/src/main/res/layout/dsl_radio_preference_item.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/expire_timer_settings_fragment.xml b/app/src/main/res/layout/expire_timer_settings_fragment.xml
new file mode 100644
index 000000000..251769830
--- /dev/null
+++ b/app/src/main/res/layout/expire_timer_settings_fragment.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/value_click_preference_item.xml b/app/src/main/res/layout/value_click_preference_item.xml
new file mode 100644
index 000000000..90c6cf4c4
--- /dev/null
+++ b/app/src/main/res/layout/value_click_preference_item.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/app_settings.xml b/app/src/main/res/navigation/app_settings.xml
index 3ba93c154..342f82e90 100644
--- a/app/src/main/res/navigation/app_settings.xml
+++ b/app/src/main/res/navigation/app_settings.xml
@@ -260,6 +260,14 @@
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
+
+
+
+
diff --git a/app/src/main/res/navigation/app_settings_expire_timer.xml b/app/src/main/res/navigation/app_settings_expire_timer.xml
new file mode 100644
index 000000000..658a2719f
--- /dev/null
+++ b/app/src/main/res/navigation/app_settings_expire_timer.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index a247be719..ad913e201 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -360,4 +360,34 @@
- @string/MediaOverviewActivity_Storage_used
+
+ - @string/ExpireTimerSettingsFragment__off
+ - @string/ExpireTimerSettingsFragment__4_weeks
+ - @string/ExpireTimerSettingsFragment__1_week
+ - @string/ExpireTimerSettingsFragment__1_day
+ - @string/ExpireTimerSettingsFragment__8_hours
+ - @string/ExpireTimerSettingsFragment__1_hour
+ - @string/ExpireTimerSettingsFragment__5_minutes
+ - @string/ExpireTimerSettingsFragment__30_seconds
+
+
+
+ - 0
+ - 2419200
+ - 604800
+ - 86400
+ - 28800
+ - 3600
+ - 300
+ - 30
+
+
+
+ - @string/CustomExpireTimerSelectorView__second
+ - @string/CustomExpireTimerSelectorView__minute
+ - @string/CustomExpireTimerSelectorView__hour
+ - @string/CustomExpireTimerSelectorView__day
+ - @string/CustomExpireTimerSelectorView__week
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3da51b48a..52bf430d3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1907,6 +1907,7 @@
No groups in common. Review requests carefully.
No contacts in this group. Review requests carefully.
View
+ The disappearing message time will be set to %1$s when you message them.
Play … Pause
@@ -3415,15 +3416,39 @@
Blocked
%1$d contacts
Messaging
+ Disappearing messages
App security
Block screenshots in the recents list and inside the app
Signal messages and calls, always relay calls, and sealed sender
+ Default timer for new chats
+ Set a default disappearing message timer for all new chats started by you.
https://signal.org/blog/sealed-sender
Show status icon
Show an icon in message details when they were delivered using sealed sender.
+
+ When enabled, new messages sent and received in new chats started by you will disappear after they have been seen.
+ When enabled, new messages sent and received in this chat will disappear after they have been seen.
+ Off
+ 4 weeks
+ 1 week
+ 1 day
+ 8 hours
+ 1 hour
+ 5 minutes
+ 30 seconds
+ Custom time
+ Set
+ Save
+
+ Second
+ Minute
+ Hour
+ Day
+ Week
+
Support center
Contact us
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java
index d89d76d44..8a1fc93f6 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java
@@ -127,6 +127,10 @@ public final class SignalAccountRecord implements SignalRecord {
diff.add("Payments");
}
+ if (this.getUniversalExpireTimer() != that.getUniversalExpireTimer()) {
+ diff.add("UniversalExpireTimer");
+ }
+
if (!Objects.equals(this.isPrimarySendsSms(), that.isPrimarySendsSms())) {
diff.add("PrimarySendsSms");
}
@@ -209,6 +213,10 @@ public final class SignalAccountRecord implements SignalRecord {
return payments;
}
+ public int getUniversalExpireTimer() {
+ return proto.getUniversalExpireTimer();
+ }
+
public boolean isPrimarySendsSms() {
return proto.getPrimarySendsSms();
}
@@ -467,6 +475,11 @@ public final class SignalAccountRecord implements SignalRecord {
return this;
}
+ public Builder setUniversalExpireTimer(int timer) {
+ builder.setUniversalExpireTimer(timer);
+ return this;
+ }
+
public Builder setPrimarySendsSms(boolean primarySendsSms) {
builder.setPrimarySendsSms(primarySendsSms);
return this;
diff --git a/libsignal/service/src/main/proto/SignalStorage.proto b/libsignal/service/src/main/proto/SignalStorage.proto
index b9af99e22..64d2bc71a 100644
--- a/libsignal/service/src/main/proto/SignalStorage.proto
+++ b/libsignal/service/src/main/proto/SignalStorage.proto
@@ -144,5 +144,6 @@ message AccountRecord {
repeated PinnedConversation pinnedConversations = 14;
bool preferContactAvatars = 15;
Payments payments = 16;
+ uint32 universalExpireTimer = 17;
bool primarySendsSms = 18;
}