kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add universal disappearing messages.
rodzic
8c6a88374b
commit
defd5e8047
|
@ -305,6 +305,11 @@
|
||||||
android:windowSoftInputMode="stateAlwaysHidden"
|
android:windowSoftInputMode="stateAlwaysHidden"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
|
<activity android:name=".recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
|
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||||
|
android:windowSoftInputMode="adjustResize"/>
|
||||||
|
|
||||||
<activity android:name=".DatabaseMigrationActivity"
|
<activity android:name=".DatabaseMigrationActivity"
|
||||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
|
|
@ -25,7 +25,7 @@ open class DSLSettingsActivity : PassphraseRequiredActivity() {
|
||||||
throw IllegalStateException("No navgraph id was passed to activity")
|
throw IllegalStateException("No navgraph id was passed to activity")
|
||||||
}
|
}
|
||||||
|
|
||||||
val fragment: NavHostFragment = NavHostFragment.create(navGraphId)
|
val fragment: NavHostFragment = NavHostFragment.create(navGraphId, intent.getBundleExtra(ARG_START_BUNDLE))
|
||||||
|
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.nav_host_fragment, fragment)
|
.replace(R.id.nav_host_fragment, fragment)
|
||||||
|
@ -61,6 +61,7 @@ open class DSLSettingsActivity : PassphraseRequiredActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ARG_NAV_GRAPH = "nav_graph"
|
const val ARG_NAV_GRAPH = "nav_graph"
|
||||||
|
const val ARG_START_BUNDLE = "start_bundle"
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class OnBackPressed : OnBackPressedCallback(true) {
|
private inner class OnBackPressed : OnBackPressedCallback(true) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.text.method.LinkMovementMethod
|
||||||
import android.text.style.ClickableSpan
|
import android.text.style.ClickableSpan
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.RadioButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -26,6 +27,7 @@ class DSLSettingsAdapter : MappingAdapter() {
|
||||||
registerFactory(DividerPreference::class.java, LayoutFactory(::DividerPreferenceViewHolder, R.layout.dsl_divider_item))
|
registerFactory(DividerPreference::class.java, LayoutFactory(::DividerPreferenceViewHolder, R.layout.dsl_divider_item))
|
||||||
registerFactory(SectionHeaderPreference::class.java, LayoutFactory(::SectionHeaderPreferenceViewHolder, R.layout.dsl_section_header))
|
registerFactory(SectionHeaderPreference::class.java, LayoutFactory(::SectionHeaderPreferenceViewHolder, R.layout.dsl_section_header))
|
||||||
registerFactory(SwitchPreference::class.java, LayoutFactory(::SwitchPreferenceViewHolder, R.layout.dsl_switch_preference_item))
|
registerFactory(SwitchPreference::class.java, LayoutFactory(::SwitchPreferenceViewHolder, R.layout.dsl_switch_preference_item))
|
||||||
|
registerFactory(RadioPreference::class.java, LayoutFactory(::RadioPreferenceViewHolder, R.layout.dsl_radio_preference_item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,6 +152,19 @@ class SwitchPreferenceViewHolder(itemView: View) : PreferenceViewHolder<SwitchPr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RadioPreferenceViewHolder(itemView: View) : PreferenceViewHolder<RadioPreference>(itemView) {
|
||||||
|
|
||||||
|
private val radioButton: RadioButton = itemView.findViewById(R.id.radio_widget)
|
||||||
|
|
||||||
|
override fun bind(model: RadioPreference) {
|
||||||
|
super.bind(model)
|
||||||
|
radioButton.isChecked = model.isChecked
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
model.onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ExternalLinkPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ExternalLinkPreference>(itemView) {
|
class ExternalLinkPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ExternalLinkPreference>(itemView) {
|
||||||
override fun bind(model: ExternalLinkPreference) {
|
override fun bind(model: ExternalLinkPreference) {
|
||||||
super.bind(model)
|
super.bind(model)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.EdgeEffect
|
import android.widget.EdgeEffect
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.annotation.MenuRes
|
import androidx.annotation.MenuRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
@ -14,8 +15,9 @@ import org.thoughtcrime.securesms.R
|
||||||
|
|
||||||
abstract class DSLSettingsFragment(
|
abstract class DSLSettingsFragment(
|
||||||
@StringRes private val titleId: Int,
|
@StringRes private val titleId: Int,
|
||||||
@MenuRes private val menuId: Int = -1
|
@MenuRes private val menuId: Int = -1,
|
||||||
) : Fragment(R.layout.dsl_settings_fragment) {
|
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment
|
||||||
|
) : Fragment(layoutId) {
|
||||||
|
|
||||||
private lateinit var recyclerView: RecyclerView
|
private lateinit var recyclerView: RecyclerView
|
||||||
private lateinit var toolbarShadowHelper: ToolbarShadowHelper
|
private lateinit var toolbarShadowHelper: ToolbarShadowHelper
|
||||||
|
|
|
@ -6,12 +6,15 @@ import android.content.Intent
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.style.TextAppearanceSpan
|
import android.text.style.TextAppearanceSpan
|
||||||
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import mobi.upod.timedurationpicker.TimeDurationPicker
|
import mobi.upod.timedurationpicker.TimeDurationPicker
|
||||||
|
@ -19,10 +22,14 @@ import mobi.upod.timedurationpicker.TimeDurationPickerDialog
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.PassphraseChangeActivity
|
import org.thoughtcrime.securesms.PassphraseChangeActivity
|
||||||
import org.thoughtcrime.securesms.R
|
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.DSLConfiguration
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
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.components.settings.configure
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
||||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
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.service.KeyCachingService
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||||
|
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||||
import org.thoughtcrime.securesms.util.SpanUtil
|
import org.thoughtcrime.securesms.util.SpanUtil
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
import java.lang.Integer.max
|
import java.lang.Integer.max
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.LinkedHashMap
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
import kotlin.collections.LinkedHashMap
|
||||||
|
|
||||||
private val TAG = Log.tag(PrivacySettingsFragment::class.java)
|
private val TAG = Log.tag(PrivacySettingsFragment::class.java)
|
||||||
|
|
||||||
|
@ -62,6 +71,8 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
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 sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
val repository = PrivacySettingsRepository()
|
val repository = PrivacySettingsRepository()
|
||||||
val factory = PrivacySettingsViewModel.Factory(sharedPreferences, repository)
|
val factory = PrivacySettingsViewModel.Factory(sharedPreferences, repository)
|
||||||
|
@ -129,6 +140,23 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||||
|
|
||||||
dividerPref()
|
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)
|
sectionHeaderPref(R.string.PrivacySettingsFragment__app_security)
|
||||||
|
|
||||||
if (state.isObsoletePasswordEnabled) {
|
if (state.isObsoletePasswordEnabled) {
|
||||||
|
@ -141,7 +169,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||||
setTitle(R.string.ApplicationPreferencesActivity_disable_passphrase)
|
setTitle(R.string.ApplicationPreferencesActivity_disable_passphrase)
|
||||||
setMessage(R.string.ApplicationPreferencesActivity_this_will_permanently_unlock_signal_and_message_notifications)
|
setMessage(R.string.ApplicationPreferencesActivity_this_will_permanently_unlock_signal_and_message_notifications)
|
||||||
setIcon(R.drawable.ic_warning)
|
setIcon(R.drawable.ic_warning)
|
||||||
setPositiveButton(R.string.ApplicationPreferencesActivity_disable) { dialog, which ->
|
setPositiveButton(R.string.ApplicationPreferencesActivity_disable) { _, _ ->
|
||||||
MasterSecretUtil.changeMasterSecretPassphrase(
|
MasterSecretUtil.changeMasterSecretPassphrase(
|
||||||
activity,
|
activity,
|
||||||
KeyCachingService.getMasterSecret(context),
|
KeyCachingService.getMasterSecret(context),
|
||||||
|
@ -395,4 +423,31 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ValueClickPreference(
|
||||||
|
val value: DSLSettingsText,
|
||||||
|
val clickPreference: ClickPreference
|
||||||
|
) : PreferenceModel<ValueClickPreference>(
|
||||||
|
title = clickPreference.title,
|
||||||
|
summary = clickPreference.summary,
|
||||||
|
iconId = clickPreference.iconId,
|
||||||
|
isEnabled = clickPreference.isEnabled
|
||||||
|
) {
|
||||||
|
override fun areContentsTheSame(newItem: ValueClickPreference): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) &&
|
||||||
|
clickPreference == newItem.clickPreference &&
|
||||||
|
value == newItem.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ValueClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ValueClickPreference>(itemView) {
|
||||||
|
private val clickPreferenceViewHolder = ClickPreferenceViewHolder(itemView)
|
||||||
|
private val valueText: TextView = findViewById(R.id.value_client_preference_value)
|
||||||
|
|
||||||
|
override fun bind(model: ValueClickPreference) {
|
||||||
|
super.bind(model)
|
||||||
|
clickPreferenceViewHolder.bind(model.clickPreference)
|
||||||
|
valueText.text = model.value.resolve(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,5 +14,6 @@ data class PrivacySettingsState(
|
||||||
val incognitoKeyboard: Boolean,
|
val incognitoKeyboard: Boolean,
|
||||||
val isObsoletePasswordEnabled: Boolean,
|
val isObsoletePasswordEnabled: Boolean,
|
||||||
val isObsoletePasswordTimeoutEnabled: Boolean,
|
val isObsoletePasswordTimeoutEnabled: Boolean,
|
||||||
val obsoletePasswordTimeout: Int
|
val obsoletePasswordTimeout: Int,
|
||||||
|
val universalExpireTimer: Int
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,6 +24,7 @@ class PrivacySettingsViewModel(
|
||||||
fun refreshBlockedCount() {
|
fun refreshBlockedCount() {
|
||||||
repository.getBlockedCount { count ->
|
repository.getBlockedCount { count ->
|
||||||
store.update { it.copy(blockedCount = count) }
|
store.update { it.copy(blockedCount = count) }
|
||||||
|
refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +100,8 @@ class PrivacySettingsViewModel(
|
||||||
findMeByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode,
|
findMeByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode,
|
||||||
isObsoletePasswordEnabled = !TextSecurePreferences.isPasswordDisabled(ApplicationDependencies.getApplication()),
|
isObsoletePasswordEnabled = !TextSecurePreferences.isPasswordDisabled(ApplicationDependencies.getApplication()),
|
||||||
isObsoletePasswordTimeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(ApplicationDependencies.getApplication()),
|
isObsoletePasswordTimeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(ApplicationDependencies.getApplication()),
|
||||||
obsoletePasswordTimeout = TextSecurePreferences.getPassphraseTimeoutInterval(ApplicationDependencies.getApplication())
|
obsoletePasswordTimeout = TextSecurePreferences.getPassphraseTimeoutInterval(ApplicationDependencies.getApplication()),
|
||||||
|
universalExpireTimer = SignalStore.settings().universalExpireTimer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for selecting a custom expire timer value.
|
||||||
|
*/
|
||||||
|
class CustomExpireTimerSelectDialog : DialogFragment() {
|
||||||
|
|
||||||
|
private lateinit var viewModel: ExpireTimerSettingsViewModel
|
||||||
|
private lateinit var selector: CustomExpireTimerSelectorView
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val dialogView: View = LayoutInflater.from(context).inflate(R.layout.custom_expire_timer_select_dialog, null, false)
|
||||||
|
selector = dialogView.findViewById(R.id.custom_expire_timer_select_dialog_selector)
|
||||||
|
|
||||||
|
val builder = MaterialAlertDialogBuilder(requireContext(), R.style.Signal_ThemeOverlay_Dialog_Rounded)
|
||||||
|
|
||||||
|
return builder.setTitle(R.string.ExpireTimerSettingsFragment__custom_time)
|
||||||
|
.setView(dialogView)
|
||||||
|
.setPositiveButton(R.string.ExpireTimerSettingsFragment__set) { _, _ ->
|
||||||
|
viewModel.select(selector.getTimer())
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
viewModel = ViewModelProvider(NavHostFragment.findNavController(this).getViewModelStoreOwner(R.id.app_settings_expire_timer))
|
||||||
|
.get(ExpireTimerSettingsViewModel::class.java)
|
||||||
|
|
||||||
|
viewModel.state.observe(this) { selector.setTimer(it.currentTimer) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DIALOG_TAG = "CustomTimerSelectDialog"
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) {
|
||||||
|
CustomExpireTimerSelectDialog().show(fragmentManager, DIALOG_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.NumberPicker
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show number pickers for value and units that are valid for expiration timer.
|
||||||
|
*/
|
||||||
|
class CustomExpireTimerSelectorView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val valuePicker: NumberPicker
|
||||||
|
private val unitPicker: NumberPicker
|
||||||
|
|
||||||
|
init {
|
||||||
|
orientation = HORIZONTAL
|
||||||
|
gravity = Gravity.CENTER
|
||||||
|
inflate(context, R.layout.custom_expire_timer_selector_view, this)
|
||||||
|
|
||||||
|
valuePicker = findViewById(R.id.custom_expire_timer_selector_value)
|
||||||
|
unitPicker = findViewById(R.id.custom_expire_timer_selector_unit)
|
||||||
|
|
||||||
|
valuePicker.minValue = TimerUnit.get(1).minValue
|
||||||
|
valuePicker.maxValue = TimerUnit.get(1).maxValue
|
||||||
|
|
||||||
|
unitPicker.minValue = 0
|
||||||
|
unitPicker.maxValue = 4
|
||||||
|
unitPicker.value = 1
|
||||||
|
unitPicker.wrapSelectorWheel = false
|
||||||
|
unitPicker.isLongClickable = false
|
||||||
|
unitPicker.displayedValues = context.resources.getStringArray(R.array.CustomExpireTimerSelectorView__unit_labels)
|
||||||
|
unitPicker.setOnValueChangedListener { _, _, newValue -> unitChange(newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTimer(timer: Int?) {
|
||||||
|
if (timer == null || timer == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
TimerUnit.values()
|
||||||
|
.find { (timer / it.valueMultiplier) < it.maxValue }
|
||||||
|
?.let { timerUnit ->
|
||||||
|
valuePicker.value = (timer / timerUnit.valueMultiplier).toInt()
|
||||||
|
unitPicker.value = TimerUnit.values().indexOf(timerUnit)
|
||||||
|
unitChange(unitPicker.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTimer(): Int {
|
||||||
|
return valuePicker.value * TimerUnit.get(unitPicker.value).valueMultiplier.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unitChange(newValue: Int) {
|
||||||
|
val timerUnit: TimerUnit = TimerUnit.values()[newValue]
|
||||||
|
|
||||||
|
valuePicker.minValue = timerUnit.minValue
|
||||||
|
valuePicker.maxValue = timerUnit.maxValue
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class TimerUnit(val minValue: Int, val maxValue: Int, val valueMultiplier: Long) {
|
||||||
|
SECONDS(1, 59, TimeUnit.SECONDS.toSeconds(1)),
|
||||||
|
MINUTES(1, 59, TimeUnit.MINUTES.toSeconds(1)),
|
||||||
|
HOURS(1, 23, TimeUnit.HOURS.toSeconds(1)),
|
||||||
|
DAYS(1, 6, TimeUnit.DAYS.toSeconds(1)),
|
||||||
|
WEEKS(1, 4, TimeUnit.DAYS.toSeconds(7));
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun get(value: Int) = values()[value]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.dd.CircularProgressButton
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||||
|
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.ProcessState
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.distinctUntilChanged
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Depending on the arguments, can be used to set the universal expire timer, set expire timer
|
||||||
|
* for a individual or group recipient, or select a value and return it via result.
|
||||||
|
*/
|
||||||
|
class ExpireTimerSettingsFragment : DSLSettingsFragment(
|
||||||
|
titleId = R.string.PrivacySettingsFragment__disappearing_messages,
|
||||||
|
layoutId = R.layout.expire_timer_settings_fragment
|
||||||
|
) {
|
||||||
|
|
||||||
|
private lateinit var save: CircularProgressButton
|
||||||
|
private lateinit var viewModel: ExpireTimerSettingsViewModel
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
save = view.findViewById(R.id.timer_select_fragment_save)
|
||||||
|
save.setOnClickListener { viewModel.save() }
|
||||||
|
adjustListPaddingForSaveButton(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustListPaddingForSaveButton(view: View) {
|
||||||
|
val recycler: RecyclerView = view.findViewById(R.id.recycler)
|
||||||
|
recycler.setPadding(recycler.paddingLeft, recycler.paddingTop, recycler.paddingRight, ViewUtil.dpToPx(80))
|
||||||
|
recycler.clipToPadding = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||||
|
val provider = ViewModelProvider(
|
||||||
|
NavHostFragment.findNavController(this).getViewModelStoreOwner(R.id.app_settings_expire_timer),
|
||||||
|
ExpireTimerSettingsViewModel.Factory(requireContext(), arguments.toConfig())
|
||||||
|
)
|
||||||
|
viewModel = provider.get(ExpireTimerSettingsViewModel::class.java)
|
||||||
|
|
||||||
|
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||||
|
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.state.distinctUntilChanged(ExpireTimerSettingsState::saveState).observe(viewLifecycleOwner) { state ->
|
||||||
|
when (val saveState: ProcessState<Int> = state.saveState) {
|
||||||
|
is ProcessState.Working -> {
|
||||||
|
save.isClickable = false
|
||||||
|
save.isIndeterminateProgressMode = true
|
||||||
|
save.progress = 50
|
||||||
|
}
|
||||||
|
is ProcessState.Success -> {
|
||||||
|
if (state.isGroupCreate) {
|
||||||
|
requireActivity().setResult(Activity.RESULT_OK, Intent().putExtra(FOR_RESULT_VALUE, saveState.result))
|
||||||
|
}
|
||||||
|
save.isClickable = false
|
||||||
|
requireActivity().onNavigateUp()
|
||||||
|
}
|
||||||
|
is ProcessState.Failure -> {
|
||||||
|
val groupChangeFailureReason: GroupChangeFailureReason = saveState.throwable?.let(GroupChangeFailureReason::fromException) ?: GroupChangeFailureReason.OTHER
|
||||||
|
Toast.makeText(context, GroupErrors.getUserDisplayMessage(groupChangeFailureReason), Toast.LENGTH_LONG).show()
|
||||||
|
viewModel.resetError()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
save.isClickable = true
|
||||||
|
save.isIndeterminateProgressMode = false
|
||||||
|
save.progress = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfiguration(state: ExpireTimerSettingsState): DSLConfiguration {
|
||||||
|
return configure {
|
||||||
|
textPref(
|
||||||
|
summary = DSLSettingsText.from(
|
||||||
|
if (state.isForRecipient) {
|
||||||
|
R.string.ExpireTimerSettingsFragment__when_enabled_new_messages_sent_and_received_in_this_chat_will_disappear_after_they_have_been_seen
|
||||||
|
} else {
|
||||||
|
R.string.ExpireTimerSettingsFragment__when_enabled_new_messages_sent_and_received_in_new_chats_started_by_you_will_disappear_after_they_have_been_seen
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val labels: Array<String> = resources.getStringArray(R.array.ExpireTimerSettingsFragment__labels)
|
||||||
|
val values: Array<Int> = resources.getIntArray(R.array.ExpireTimerSettingsFragment__values).toTypedArray()
|
||||||
|
|
||||||
|
var hasCustomValue = true
|
||||||
|
labels.zip(values).forEach { (label, value) ->
|
||||||
|
radioPref(
|
||||||
|
title = DSLSettingsText.from(label),
|
||||||
|
isChecked = state.currentTimer == value,
|
||||||
|
onClick = { viewModel.select(value) }
|
||||||
|
)
|
||||||
|
hasCustomValue = hasCustomValue && state.currentTimer != value
|
||||||
|
}
|
||||||
|
|
||||||
|
radioPref(
|
||||||
|
title = DSLSettingsText.from(R.string.ExpireTimerSettingsFragment__custom_time),
|
||||||
|
summary = if (hasCustomValue) DSLSettingsText.from(ExpirationUtil.getExpirationDisplayValue(requireContext(), state.currentTimer)) else null,
|
||||||
|
isChecked = hasCustomValue,
|
||||||
|
onClick = { NavHostFragment.findNavController(this@ExpireTimerSettingsFragment).navigate(R.id.action_expireTimerSettingsFragment_to_customExpireTimerSelectDialog) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val FOR_RESULT_VALUE = "for_result_value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Bundle?.toConfig(): ExpireTimerSettingsViewModel.Config {
|
||||||
|
if (this == null) {
|
||||||
|
return ExpireTimerSettingsViewModel.Config()
|
||||||
|
}
|
||||||
|
|
||||||
|
val safeArguments: ExpireTimerSettingsFragmentArgs = ExpireTimerSettingsFragmentArgs.fromBundle(this)
|
||||||
|
return ExpireTimerSettingsViewModel.Config(
|
||||||
|
recipientId = safeArguments.recipientId,
|
||||||
|
forResultMode = safeArguments.forResultMode,
|
||||||
|
initialValue = safeArguments.initialValue
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupChangeException
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupManager
|
||||||
|
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.sms.MessageSender
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
private val TAG: String = Log.tag(ExpireTimerSettingsRepository::class.java)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide operations to set expire timer for individuals and groups.
|
||||||
|
*/
|
||||||
|
class ExpireTimerSettingsRepository(val context: Context) {
|
||||||
|
|
||||||
|
fun setExpiration(recipientId: RecipientId, newExpirationTime: Int, consumer: (Result<Int>) -> Unit) {
|
||||||
|
SignalExecutors.BOUNDED.execute {
|
||||||
|
val recipient = Recipient.resolved(recipientId)
|
||||||
|
if (recipient.groupId.isPresent && recipient.groupId.get().isPush) {
|
||||||
|
try {
|
||||||
|
GroupManager.updateGroupTimer(context, recipient.groupId.get().requirePush(), newExpirationTime)
|
||||||
|
consumer.invoke(Result.success(newExpirationTime))
|
||||||
|
} catch (e: GroupChangeException) {
|
||||||
|
Log.w(TAG, e)
|
||||||
|
consumer.invoke(Result.failure(e))
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.w(TAG, e)
|
||||||
|
consumer.invoke(Result.failure(e))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipientId, newExpirationTime)
|
||||||
|
val outgoingMessage = OutgoingExpirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L)
|
||||||
|
MessageSender.send(context, outgoingMessage, getThreadId(recipientId), false, null)
|
||||||
|
consumer.invoke(Result.success(newExpirationTime))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private fun getThreadId(recipientId: RecipientId): Long {
|
||||||
|
val threadDatabase: ThreadDatabase = DatabaseFactory.getThreadDatabase(context)
|
||||||
|
val recipient: Recipient = Recipient.resolved(recipientId)
|
||||||
|
return threadDatabase.getThreadIdFor(recipient)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.ProcessState
|
||||||
|
|
||||||
|
data class ExpireTimerSettingsState(
|
||||||
|
val initialTimer: Int = 0,
|
||||||
|
val userSetTimer: Int? = null,
|
||||||
|
val saveState: ProcessState<Int> = ProcessState.Idle(),
|
||||||
|
val isGroupCreate: Boolean = false,
|
||||||
|
val isForRecipient: Boolean = isGroupCreate,
|
||||||
|
) {
|
||||||
|
val currentTimer: Int
|
||||||
|
get() = userSetTimer ?: initialTimer
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package org.thoughtcrime.securesms.components.settings.app.privacy.expire
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.ProcessState
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
|
||||||
|
class ExpireTimerSettingsViewModel(val config: Config, private val repository: ExpireTimerSettingsRepository) : ViewModel() {
|
||||||
|
|
||||||
|
private val store = Store<ExpireTimerSettingsState>(ExpireTimerSettingsState(isGroupCreate = config.forResultMode))
|
||||||
|
private val recipientId: RecipientId? = config.recipientId
|
||||||
|
|
||||||
|
val state: LiveData<ExpireTimerSettingsState> = store.stateLiveData
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (recipientId != null) {
|
||||||
|
store.update(Recipient.live(recipientId).liveData) { r, s -> s.copy(initialTimer = r.expireMessages, isForRecipient = true) }
|
||||||
|
} else {
|
||||||
|
store.update { it.copy(initialTimer = config.initialValue ?: SignalStore.settings().universalExpireTimer) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun select(time: Int) {
|
||||||
|
store.update { it.copy(userSetTimer = time) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save() {
|
||||||
|
val userSetTimer: Int = store.state.currentTimer
|
||||||
|
|
||||||
|
if (userSetTimer == store.state.initialTimer) {
|
||||||
|
store.update { it.copy(saveState = ProcessState.Success(userSetTimer)) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update { it.copy(saveState = ProcessState.Working()) }
|
||||||
|
if (recipientId != null) {
|
||||||
|
repository.setExpiration(recipientId, userSetTimer) { result ->
|
||||||
|
store.update { it.copy(saveState = ProcessState.fromResult(result)) }
|
||||||
|
}
|
||||||
|
} else if (config.forResultMode) {
|
||||||
|
store.update { it.copy(saveState = ProcessState.Success(userSetTimer)) }
|
||||||
|
} else {
|
||||||
|
SignalStore.settings().universalExpireTimer = userSetTimer
|
||||||
|
store.update { it.copy(saveState = ProcessState.Success(userSetTimer)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetError() {
|
||||||
|
store.update { it.copy(saveState = ProcessState.Idle()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory(context: Context, private val config: Config) : ViewModelProvider.Factory {
|
||||||
|
val repository = ExpireTimerSettingsRepository(context.applicationContext)
|
||||||
|
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return requireNotNull(modelClass.cast(ExpireTimerSettingsViewModel(config, repository)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Config(
|
||||||
|
val recipientId: RecipientId? = null,
|
||||||
|
val forResultMode: Boolean = false,
|
||||||
|
val initialValue: Int? = null
|
||||||
|
)
|
||||||
|
}
|
|
@ -56,6 +56,17 @@ class DSLConfiguration {
|
||||||
children.add(preference)
|
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(
|
fun clickPref(
|
||||||
title: DSLSettingsText,
|
title: DSLSettingsText,
|
||||||
summary: DSLSettingsText? = null,
|
summary: DSLSettingsText? = null,
|
||||||
|
@ -175,11 +186,23 @@ class SwitchPreference(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RadioPreference(
|
||||||
|
title: DSLSettingsText,
|
||||||
|
summary: DSLSettingsText? = null,
|
||||||
|
isEnabled: Boolean,
|
||||||
|
val isChecked: Boolean,
|
||||||
|
val onClick: () -> Unit
|
||||||
|
) : PreferenceModel<RadioPreference>(title = title, summary = summary, isEnabled = isEnabled) {
|
||||||
|
override fun areContentsTheSame(newItem: RadioPreference): Boolean {
|
||||||
|
return super.areContentsTheSame(newItem) && isChecked == newItem.isChecked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ClickPreference(
|
class ClickPreference(
|
||||||
override val title: DSLSettingsText,
|
override val title: DSLSettingsText,
|
||||||
override val summary: DSLSettingsText?,
|
override val summary: DSLSettingsText? = null,
|
||||||
@DrawableRes override val iconId: Int,
|
@DrawableRes override val iconId: Int = UNSET,
|
||||||
isEnabled: Boolean,
|
isEnabled: Boolean = true,
|
||||||
val onClick: () -> Unit
|
val onClick: () -> Unit
|
||||||
) : PreferenceModel<ClickPreference>(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled)
|
) : PreferenceModel<ClickPreference>(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled)
|
||||||
|
|
||||||
|
|
|
@ -2412,6 +2412,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
if (groupCallViewModel != null) {
|
if (groupCallViewModel != null) {
|
||||||
groupCallViewModel.onRecipientChange(recipient);
|
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)
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
|
|
|
@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
||||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||||
|
@ -374,6 +375,10 @@ public class ConversationAdapter
|
||||||
this.pagingController = pagingController;
|
this.pagingController = pagingController;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isForRecipientId(@NonNull RecipientId recipientId) {
|
||||||
|
return recipient.getId().equals(recipientId);
|
||||||
|
}
|
||||||
|
|
||||||
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
|
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
|
||||||
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
|
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ final class ConversationData {
|
||||||
private final int jumpToPosition;
|
private final int jumpToPosition;
|
||||||
private final int threadSize;
|
private final int threadSize;
|
||||||
private final MessageRequestData messageRequestData;
|
private final MessageRequestData messageRequestData;
|
||||||
|
private final boolean showUniversalExpireTimerMessage;
|
||||||
|
|
||||||
ConversationData(long threadId,
|
ConversationData(long threadId,
|
||||||
long lastSeen,
|
long lastSeen,
|
||||||
|
@ -22,16 +23,18 @@ final class ConversationData {
|
||||||
boolean hasSent,
|
boolean hasSent,
|
||||||
int jumpToPosition,
|
int jumpToPosition,
|
||||||
int threadSize,
|
int threadSize,
|
||||||
@NonNull MessageRequestData messageRequestData)
|
@NonNull MessageRequestData messageRequestData,
|
||||||
|
boolean showUniversalExpireTimerMessage)
|
||||||
{
|
{
|
||||||
this.threadId = threadId;
|
this.threadId = threadId;
|
||||||
this.lastSeen = lastSeen;
|
this.lastSeen = lastSeen;
|
||||||
this.lastSeenPosition = lastSeenPosition;
|
this.lastSeenPosition = lastSeenPosition;
|
||||||
this.lastScrolledPosition = lastScrolledPosition;
|
this.lastScrolledPosition = lastScrolledPosition;
|
||||||
this.hasSent = hasSent;
|
this.hasSent = hasSent;
|
||||||
this.jumpToPosition = jumpToPosition;
|
this.jumpToPosition = jumpToPosition;
|
||||||
this.threadSize = threadSize;
|
this.threadSize = threadSize;
|
||||||
this.messageRequestData = messageRequestData;
|
this.messageRequestData = messageRequestData;
|
||||||
|
this.showUniversalExpireTimerMessage = showUniversalExpireTimerMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getThreadId() {
|
public long getThreadId() {
|
||||||
|
@ -74,6 +77,10 @@ final class ConversationData {
|
||||||
return messageRequestData;
|
return messageRequestData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean showUniversalExpireTimerMessage() {
|
||||||
|
return showUniversalExpireTimerMessage;
|
||||||
|
}
|
||||||
|
|
||||||
static final class MessageRequestData {
|
static final class MessageRequestData {
|
||||||
|
|
||||||
private final boolean messageRequestAccepted;
|
private final boolean messageRequestAccepted;
|
||||||
|
|
|
@ -35,17 +35,21 @@ class ConversationDataSource implements PagedDataSource<ConversationMessage> {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private final long threadId;
|
private final long threadId;
|
||||||
private final MessageRequestData messageRequestData;
|
private final MessageRequestData messageRequestData;
|
||||||
|
private final boolean showUniversalExpireTimerUpdate;
|
||||||
|
|
||||||
ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData) {
|
ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData, boolean showUniversalExpireTimerUpdate) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.threadId = threadId;
|
this.threadId = threadId;
|
||||||
this.messageRequestData = messageRequestData;
|
this.messageRequestData = messageRequestData;
|
||||||
|
this.showUniversalExpireTimerUpdate = showUniversalExpireTimerUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int size() {
|
public int size() {
|
||||||
long startTime = System.currentTimeMillis();
|
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");
|
Log.d(TAG, "size() for thread " + threadId + ": " + (System.currentTimeMillis() - startTime) + " ms");
|
||||||
|
|
||||||
|
@ -71,6 +75,10 @@ class ConversationDataSource implements PagedDataSource<ConversationMessage> {
|
||||||
records.add(new InMemoryMessageRecord.NoGroupsInCommon(threadId, messageRequestData.isGroup()));
|
records.add(new InMemoryMessageRecord.NoGroupsInCommon(threadId, messageRequestData.isGroup()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showUniversalExpireTimerUpdate) {
|
||||||
|
records.add(new InMemoryMessageRecord.UniversalExpireTimerUpdate(threadId));
|
||||||
|
}
|
||||||
|
|
||||||
stopwatch.split("messages");
|
stopwatch.split("messages");
|
||||||
|
|
||||||
mentionHelper.fetchMentions(context);
|
mentionHelper.fetchMentions(context);
|
||||||
|
|
|
@ -183,38 +183,37 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
|
|
||||||
private ConversationFragmentListener listener;
|
private ConversationFragmentListener listener;
|
||||||
|
|
||||||
private LiveRecipient recipient;
|
private LiveRecipient recipient;
|
||||||
private long threadId;
|
private long threadId;
|
||||||
private boolean isReacting;
|
private boolean isReacting;
|
||||||
private ActionMode actionMode;
|
private ActionMode actionMode;
|
||||||
private Locale locale;
|
private Locale locale;
|
||||||
private FrameLayout videoContainer;
|
private FrameLayout videoContainer;
|
||||||
private RecyclerView list;
|
private RecyclerView list;
|
||||||
private RecyclerView.ItemDecoration lastSeenDecoration;
|
private RecyclerView.ItemDecoration lastSeenDecoration;
|
||||||
private RecyclerView.ItemDecoration inlineDateDecoration;
|
private RecyclerView.ItemDecoration inlineDateDecoration;
|
||||||
private ViewSwitcher topLoadMoreView;
|
private ViewSwitcher topLoadMoreView;
|
||||||
private ViewSwitcher bottomLoadMoreView;
|
private ViewSwitcher bottomLoadMoreView;
|
||||||
private ConversationTypingView typingView;
|
private ConversationTypingView typingView;
|
||||||
private View composeDivider;
|
private View composeDivider;
|
||||||
private ConversationScrollToView scrollToBottomButton;
|
private ConversationScrollToView scrollToBottomButton;
|
||||||
private ConversationScrollToView scrollToMentionButton;
|
private ConversationScrollToView scrollToMentionButton;
|
||||||
private TextView scrollDateHeader;
|
private TextView scrollDateHeader;
|
||||||
private ConversationBannerView conversationBanner;
|
private ConversationBannerView conversationBanner;
|
||||||
private ConversationBannerView emptyConversationBanner;
|
private MessageRequestViewModel messageRequestViewModel;
|
||||||
private MessageRequestViewModel messageRequestViewModel;
|
private MessageCountsViewModel messageCountsViewModel;
|
||||||
private MessageCountsViewModel messageCountsViewModel;
|
private ConversationViewModel conversationViewModel;
|
||||||
private ConversationViewModel conversationViewModel;
|
private SnapToTopDataObserver snapToTopDataObserver;
|
||||||
private SnapToTopDataObserver snapToTopDataObserver;
|
private MarkReadHelper markReadHelper;
|
||||||
private MarkReadHelper markReadHelper;
|
private Animation scrollButtonInAnimation;
|
||||||
private Animation scrollButtonInAnimation;
|
private Animation mentionButtonInAnimation;
|
||||||
private Animation mentionButtonInAnimation;
|
private Animation scrollButtonOutAnimation;
|
||||||
private Animation scrollButtonOutAnimation;
|
private Animation mentionButtonOutAnimation;
|
||||||
private Animation mentionButtonOutAnimation;
|
private OnScrollListener conversationScrollListener;
|
||||||
private OnScrollListener conversationScrollListener;
|
private int pulsePosition = -1;
|
||||||
private int pulsePosition = -1;
|
private VoiceNoteMediaController voiceNoteMediaController;
|
||||||
private VoiceNoteMediaController voiceNoteMediaController;
|
private View toolbarShadow;
|
||||||
private View toolbarShadow;
|
private Stopwatch startupStopwatch;
|
||||||
private Stopwatch startupStopwatch;
|
|
||||||
|
|
||||||
private GiphyMp4ProjectionRecycler giphyMp4ProjectionRecycler;
|
private GiphyMp4ProjectionRecycler giphyMp4ProjectionRecycler;
|
||||||
|
|
||||||
|
@ -240,15 +239,14 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||||
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
|
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
|
||||||
videoContainer = view.findViewById(R.id.video_container);
|
videoContainer = view.findViewById(R.id.video_container);
|
||||||
list = view.findViewById(android.R.id.list);
|
list = view.findViewById(android.R.id.list);
|
||||||
composeDivider = view.findViewById(R.id.compose_divider);
|
composeDivider = view.findViewById(R.id.compose_divider);
|
||||||
|
|
||||||
scrollToBottomButton = view.findViewById(R.id.scroll_to_bottom);
|
scrollToBottomButton = view.findViewById(R.id.scroll_to_bottom);
|
||||||
scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
|
scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
|
||||||
scrollDateHeader = view.findViewById(R.id.scroll_date_header);
|
scrollDateHeader = view.findViewById(R.id.scroll_date_header);
|
||||||
emptyConversationBanner = view.findViewById(R.id.empty_conversation_banner);
|
toolbarShadow = requireActivity().findViewById(R.id.conversation_toolbar_shadow);
|
||||||
toolbarShadow = requireActivity().findViewById(R.id.conversation_toolbar_shadow);
|
|
||||||
|
|
||||||
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
|
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
|
||||||
list.setHasFixedSize(false);
|
list.setHasFixedSize(false);
|
||||||
|
@ -483,7 +481,6 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
|
|
||||||
messageRequestViewModel.getRecipientInfo().observe(getViewLifecycleOwner(), recipientInfo -> {
|
messageRequestViewModel.getRecipientInfo().observe(getViewLifecycleOwner(), recipientInfo -> {
|
||||||
presentMessageRequestProfileView(requireContext(), recipientInfo, conversationBanner);
|
presentMessageRequestProfileView(requireContext(), recipientInfo, conversationBanner);
|
||||||
presentMessageRequestProfileView(requireContext(), recipientInfo, emptyConversationBanner);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
messageRequestViewModel.getMessageData().observe(getViewLifecycleOwner(), data -> {
|
messageRequestViewModel.getMessageData().observe(getViewLifecycleOwner(), data -> {
|
||||||
|
@ -606,7 +603,16 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeListAdapter() {
|
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());
|
Log.d(TAG, "Initializing adapter for " + recipient.getId());
|
||||||
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get(), new AttachmentMediaSourceFactory(requireContext()));
|
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get(), new AttachmentMediaSourceFactory(requireContext()));
|
||||||
adapter.setPagingController(conversationViewModel.getPagingController());
|
adapter.setPagingController(conversationViewModel.getPagingController());
|
||||||
|
@ -618,8 +624,6 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
|
|
||||||
setLastSeen(conversationViewModel.getLastSeen());
|
setLastSeen(conversationViewModel.getLastSeen());
|
||||||
|
|
||||||
emptyConversationBanner.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||||
@Override
|
@Override
|
||||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
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_save_attachment).setVisible(menuState.shouldShowSaveAttachmentAction());
|
||||||
menu.findItem(R.id.menu_context_resend).setVisible(menuState.shouldShowResendAction());
|
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_copy).setVisible(menuState.shouldShowCopyAction());
|
||||||
|
menu.findItem(R.id.menu_context_delete_message).setVisible(menuState.shouldShowDeleteAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable ConversationAdapter getListAdapter() {
|
private @Nullable ConversationAdapter getListAdapter() {
|
||||||
|
@ -756,7 +758,9 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
snapToTopDataObserver.requestScrollPosition(0);
|
snapToTopDataObserver.requestScrollPosition(0);
|
||||||
conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, -1);
|
conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, -1);
|
||||||
messageCountsViewModel.setThreadId(threadId);
|
messageCountsViewModel.setThreadId(threadId);
|
||||||
|
markReadHelper = new MarkReadHelper(threadId, requireContext());
|
||||||
initializeListAdapter();
|
initializeListAdapter();
|
||||||
|
initializeTypingObserver();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1229,7 +1233,6 @@ public class ConversationFragment extends LoggingFragment {
|
||||||
toolbar.getGlobalVisibleRect(rect);
|
toolbar.getGlobalVisibleRect(rect);
|
||||||
ViewUtil.setTopMargin(scrollDateHeader, rect.bottom + ViewUtil.dpToPx(8));
|
ViewUtil.setTopMargin(scrollDateHeader, rect.bottom + ViewUtil.dpToPx(8));
|
||||||
ViewUtil.setTopMargin(conversationBanner, rect.bottom + ViewUtil.dpToPx(16));
|
ViewUtil.setTopMargin(conversationBanner, rect.bottom + ViewUtil.dpToPx(16));
|
||||||
ViewUtil.setTopMargin(emptyConversationBanner, rect.bottom + ViewUtil.dpToPx(16));
|
|
||||||
toolbar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
toolbar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
import org.thoughtcrime.securesms.util.BubbleUtil;
|
import org.thoughtcrime.securesms.util.BubbleUtil;
|
||||||
|
@ -32,11 +33,11 @@ class ConversationRepository {
|
||||||
this.executor = SignalExecutors.BOUNDED;
|
this.executor = SignalExecutors.BOUNDED;
|
||||||
}
|
}
|
||||||
|
|
||||||
LiveData<ConversationData> getConversationData(long threadId, int jumpToPosition) {
|
LiveData<ConversationData> getConversationData(long threadId, @NonNull Recipient recipient, int jumpToPosition) {
|
||||||
MutableLiveData<ConversationData> liveData = new MutableLiveData<>();
|
MutableLiveData<ConversationData> liveData = new MutableLiveData<>();
|
||||||
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
liveData.postValue(getConversationDataInternal(threadId, jumpToPosition));
|
liveData.postValue(getConversationDataInternal(threadId, recipient, jumpToPosition));
|
||||||
});
|
});
|
||||||
|
|
||||||
return liveData;
|
return liveData;
|
||||||
|
@ -53,16 +54,17 @@ class ConversationRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull ConversationData getConversationDataInternal(long threadId, int jumpToPosition) {
|
private @NonNull ConversationData getConversationDataInternal(long threadId, @NonNull Recipient conversationRecipient, int jumpToPosition) {
|
||||||
ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId);
|
ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId);
|
||||||
int threadSize = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId);
|
int threadSize = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId);
|
||||||
long lastSeen = metadata.getLastSeen();
|
long lastSeen = metadata.getLastSeen();
|
||||||
boolean hasSent = metadata.hasSent();
|
boolean hasSent = metadata.hasSent();
|
||||||
int lastSeenPosition = 0;
|
int lastSeenPosition = 0;
|
||||||
long lastScrolled = metadata.getLastScrolled();
|
long lastScrolled = metadata.getLastScrolled();
|
||||||
int lastScrolledPosition = 0;
|
int lastScrolledPosition = 0;
|
||||||
boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId);
|
boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId);
|
||||||
ConversationData.MessageRequestData messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted);
|
ConversationData.MessageRequestData messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted);
|
||||||
|
boolean showUniversalExpireTimerUpdate = false;
|
||||||
|
|
||||||
if (lastSeen > 0) {
|
if (lastSeen > 0) {
|
||||||
lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastSeen);
|
lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastSeen);
|
||||||
|
@ -79,9 +81,8 @@ class ConversationRepository {
|
||||||
if (!isMessageRequestAccepted) {
|
if (!isMessageRequestAccepted) {
|
||||||
boolean isGroup = false;
|
boolean isGroup = false;
|
||||||
boolean recipientIsKnownOrHasGroupsInCommon = false;
|
boolean recipientIsKnownOrHasGroupsInCommon = false;
|
||||||
Recipient threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
|
if (conversationRecipient.isGroup()) {
|
||||||
if (threadRecipient.isGroup()) {
|
Optional<GroupDatabase.GroupRecord> group = DatabaseFactory.getGroupDatabase(context).getGroup(conversationRecipient.getId());
|
||||||
Optional<GroupDatabase.GroupRecord> group = DatabaseFactory.getGroupDatabase(context).getGroup(threadRecipient.getId());
|
|
||||||
if (group.isPresent()) {
|
if (group.isPresent()) {
|
||||||
List<Recipient> recipients = Recipient.resolvedList(group.get().getMembers());
|
List<Recipient> recipients = Recipient.resolvedList(group.get().getMembers());
|
||||||
for (Recipient recipient : recipients) {
|
for (Recipient recipient : recipients) {
|
||||||
|
@ -92,12 +93,20 @@ class ConversationRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isGroup = true;
|
isGroup = true;
|
||||||
} else if (threadRecipient.hasGroupsInCommon()) {
|
} else if (conversationRecipient.hasGroupsInCommon()) {
|
||||||
recipientIsKnownOrHasGroupsInCommon = true;
|
recipientIsKnownOrHasGroupsInCommon = true;
|
||||||
}
|
}
|
||||||
messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted, recipientIsKnownOrHasGroupsInCommon, isGroup);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,8 +69,11 @@ public class ConversationViewModel extends ViewModel {
|
||||||
this.pagingController = new ProxyPagingController();
|
this.pagingController = new ProxyPagingController();
|
||||||
this.messageObserver = pagingController::onDataInvalidated;
|
this.messageObserver = pagingController::onDataInvalidated;
|
||||||
|
|
||||||
LiveData<ConversationData> metadata = Transformations.switchMap(threadId, thread -> {
|
LiveData<Recipient> recipientLiveData = LiveDataUtil.mapAsync(recipientId, Recipient::resolved);
|
||||||
LiveData<ConversationData> conversationData = conversationRepository.getConversationData(thread, jumpToPosition);
|
LiveData<ThreadAndRecipient> threadAndRecipient = LiveDataUtil.combineLatest(threadId, recipientLiveData, ThreadAndRecipient::new);
|
||||||
|
|
||||||
|
LiveData<ConversationData> metadata = Transformations.switchMap(threadAndRecipient, d -> {
|
||||||
|
LiveData<ConversationData> conversationData = conversationRepository.getConversationData(d.threadId, d.recipient, jumpToPosition);
|
||||||
|
|
||||||
jumpToPosition = -1;
|
jumpToPosition = -1;
|
||||||
|
|
||||||
|
@ -94,12 +97,11 @@ public class ConversationViewModel extends ViewModel {
|
||||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver);
|
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver);
|
||||||
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), messageObserver);
|
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), messageObserver);
|
||||||
|
|
||||||
ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData);
|
ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData, data.showUniversalExpireTimerMessage());
|
||||||
PagingConfig config = new PagingConfig.Builder()
|
PagingConfig config = new PagingConfig.Builder().setPageSize(25)
|
||||||
.setPageSize(25)
|
.setBufferPages(3)
|
||||||
.setBufferPages(3)
|
.setStartIndex(Math.max(startPosition, 0))
|
||||||
.setStartIndex(Math.max(startPosition, 0))
|
.build();
|
||||||
.build();
|
|
||||||
|
|
||||||
Log.d(TAG, "Starting at position: " + startPosition + " || jumpToPosition: " + data.getJumpToPosition() + ", lastSeenPosition: " + data.getLastSeenPosition() + ", lastScrolledPosition: " + data.getLastScrolledPosition());
|
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));
|
return new Pair<>(data.getThreadId(), PagedData.create(dataSource, config));
|
||||||
|
@ -213,9 +215,20 @@ public class ConversationViewModel extends ViewModel {
|
||||||
SHOW_RECAPTCHA
|
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 {
|
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||||
@Override
|
@Override
|
||||||
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
return modelClass.cast(new ConversationViewModel());
|
return modelClass.cast(new ConversationViewModel());
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ final class MenuState {
|
||||||
private final boolean saveAttachment;
|
private final boolean saveAttachment;
|
||||||
private final boolean resend;
|
private final boolean resend;
|
||||||
private final boolean copy;
|
private final boolean copy;
|
||||||
|
private final boolean delete;
|
||||||
|
|
||||||
private MenuState(@NonNull Builder builder) {
|
private MenuState(@NonNull Builder builder) {
|
||||||
forward = builder.forward;
|
forward = builder.forward;
|
||||||
|
@ -25,6 +26,7 @@ final class MenuState {
|
||||||
saveAttachment = builder.saveAttachment;
|
saveAttachment = builder.saveAttachment;
|
||||||
resend = builder.resend;
|
resend = builder.resend;
|
||||||
copy = builder.copy;
|
copy = builder.copy;
|
||||||
|
delete = builder.delete;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean shouldShowForwardAction() {
|
boolean shouldShowForwardAction() {
|
||||||
|
@ -51,6 +53,10 @@ final class MenuState {
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean shouldShowDeleteAction() {
|
||||||
|
return delete;
|
||||||
|
}
|
||||||
|
|
||||||
static MenuState getMenuState(@NonNull Recipient conversationRecipient,
|
static MenuState getMenuState(@NonNull Recipient conversationRecipient,
|
||||||
@NonNull Set<MessageRecord> messageRecords,
|
@NonNull Set<MessageRecord> messageRecords,
|
||||||
boolean shouldShowMessageRequest)
|
boolean shouldShowMessageRequest)
|
||||||
|
@ -62,11 +68,14 @@ final class MenuState {
|
||||||
boolean sharedContact = false;
|
boolean sharedContact = false;
|
||||||
boolean viewOnce = false;
|
boolean viewOnce = false;
|
||||||
boolean remoteDelete = false;
|
boolean remoteDelete = false;
|
||||||
|
boolean hasInMemory = false;
|
||||||
|
|
||||||
for (MessageRecord messageRecord : messageRecords) {
|
for (MessageRecord messageRecord : messageRecords) {
|
||||||
if (isActionMessage(messageRecord))
|
if (isActionMessage(messageRecord)) {
|
||||||
{
|
|
||||||
actionMessage = true;
|
actionMessage = true;
|
||||||
|
if (messageRecord.isInMemoryMessageRecord()) {
|
||||||
|
hasInMemory = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageRecord.getBody().length() > 0) {
|
if (messageRecord.getBody().length() > 0) {
|
||||||
|
@ -109,6 +118,7 @@ final class MenuState {
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText)
|
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText)
|
||||||
|
.shouldShowDeleteAction(!hasInMemory)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +144,8 @@ final class MenuState {
|
||||||
messageRecord.isIdentityDefault() ||
|
messageRecord.isIdentityDefault() ||
|
||||||
messageRecord.isProfileChange() ||
|
messageRecord.isProfileChange() ||
|
||||||
messageRecord.isGroupV1MigrationEvent() ||
|
messageRecord.isGroupV1MigrationEvent() ||
|
||||||
messageRecord.isFailedDecryptionType();
|
messageRecord.isFailedDecryptionType() ||
|
||||||
|
messageRecord.isInMemoryMessageRecord();
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static class Builder {
|
private final static class Builder {
|
||||||
|
@ -145,6 +156,7 @@ final class MenuState {
|
||||||
private boolean saveAttachment;
|
private boolean saveAttachment;
|
||||||
private boolean resend;
|
private boolean resend;
|
||||||
private boolean copy;
|
private boolean copy;
|
||||||
|
private boolean delete;
|
||||||
|
|
||||||
@NonNull Builder shouldShowForwardAction(boolean forward) {
|
@NonNull Builder shouldShowForwardAction(boolean forward) {
|
||||||
this.forward = forward;
|
this.forward = forward;
|
||||||
|
@ -176,6 +188,11 @@ final class MenuState {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull Builder shouldShowDeleteAction(boolean delete) {
|
||||||
|
this.delete = delete;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
MenuState build() {
|
MenuState build() {
|
||||||
return new MenuState(this);
|
return new MenuState(this);
|
||||||
|
|
|
@ -77,6 +77,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||||
public abstract int getMessageCountForThread(long threadId);
|
public abstract int getMessageCountForThread(long threadId);
|
||||||
public abstract int getMessageCountForThread(long threadId, long beforeTime);
|
public abstract int getMessageCountForThread(long threadId, long beforeTime);
|
||||||
abstract int getMessageCountForThreadSummary(long threadId);
|
abstract int getMessageCountForThreadSummary(long threadId);
|
||||||
|
public abstract boolean hasMeaningfulMessage(long threadId);
|
||||||
public abstract Optional<MmsNotificationInfo> getNotification(long messageId);
|
public abstract Optional<MmsNotificationInfo> getNotification(long messageId);
|
||||||
|
|
||||||
public abstract Cursor getExpirationStartedMessages();
|
public abstract Cursor getExpirationStartedMessages();
|
||||||
|
|
|
@ -620,6 +620,19 @@ public class MmsDatabase extends MessageDatabase {
|
||||||
return 0;
|
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
|
@Override
|
||||||
public void addFailures(long messageId, List<NetworkFailure> failure) {
|
public void addFailures(long messageId, List<NetworkFailure> failure) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -337,6 +337,15 @@ public class MmsSmsDatabase extends Database {
|
||||||
return count;
|
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) {
|
public long getThreadForMessageId(long messageId) {
|
||||||
long id = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
|
long id = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId);
|
||||||
|
|
||||||
|
|
|
@ -233,14 +233,11 @@ public class SmsDatabase extends MessageDatabase {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMessageCountForThreadSummary(long threadId) {
|
public int getMessageCountForThreadSummary(long threadId) {
|
||||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||||
|
SqlUtil.Query query = buildMeaningfulMessagesQuery(threadId);
|
||||||
|
String[] cols = { "COUNT(*)" };
|
||||||
|
|
||||||
String[] cols = { "COUNT(*)" };
|
try (Cursor cursor = db.query(TABLE_NAME, cols, query.getWhere(), query.getWhereArgs(), null, null, null)) {
|
||||||
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)) {
|
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
int count = cursor.getInt(0);
|
int count = cursor.getInt(0);
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
|
@ -286,6 +283,22 @@ public class SmsDatabase extends MessageDatabase {
|
||||||
return 0;
|
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
|
@Override
|
||||||
public void markAsEndSession(long id) {
|
public void markAsEndSession(long id) {
|
||||||
updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.END_SESSION_BIT);
|
updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.END_SESSION_BIT);
|
||||||
|
|
|
@ -176,8 +176,12 @@ public class ThreadDatabase extends Database {
|
||||||
|
|
||||||
contentValues.put(MESSAGE_COUNT, 0);
|
contentValues.put(MESSAGE_COUNT, 0);
|
||||||
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
return db.insert(TABLE_NAME, null, contentValues);
|
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,
|
private void updateThread(long threadId, long count, String body, @Nullable Uri attachment,
|
||||||
|
|
|
@ -475,7 +475,7 @@ final class GroupsV2UpdateMessageProducer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
void describeNewTimer(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||||
|
|
||||||
if (change.hasNewTimer()) {
|
if (change.hasNewTimer()) {
|
||||||
|
|
|
@ -7,7 +7,9 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
|
@ -16,6 +18,9 @@ import java.util.Collections;
|
||||||
*/
|
*/
|
||||||
public class InMemoryMessageRecord extends MessageRecord {
|
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,
|
private InMemoryMessageRecord(long id,
|
||||||
String body,
|
String body,
|
||||||
Recipient conversationRecipient,
|
Recipient conversationRecipient,
|
||||||
|
@ -78,7 +83,7 @@ public class InMemoryMessageRecord extends MessageRecord {
|
||||||
private final boolean isGroup;
|
private final boolean isGroup;
|
||||||
|
|
||||||
public NoGroupsInCommon(long threadId, 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;
|
this.isGroup = isGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,4 +113,28 @@ public class InMemoryMessageRecord extends MessageRecord {
|
||||||
return R.string.ConversationUpdateItem_learn_more;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,11 +240,18 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||||
|
|
||||||
if (decryptedGroupV2Context.hasChange() && (decryptedGroupV2Context.getGroupState().getRevision() != 0 || decryptedGroupV2Context.hasPreviousGroupState())) {
|
if (decryptedGroupV2Context.hasChange() && (decryptedGroupV2Context.getGroupState().getRevision() != 0 || decryptedGroupV2Context.hasPreviousGroupState())) {
|
||||||
return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(decryptedGroupV2Context.getPreviousGroupState(), decryptedGroupV2Context.getChange()));
|
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 {
|
} else {
|
||||||
return updateMessageProducer.describeNewGroup(decryptedGroupV2Context.getGroupState(), decryptedGroupV2Context.getChange());
|
List<UpdateDescription> 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) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, "GV2 Message update detail could not be read", e);
|
Log.w(TAG, "GV2 Message update detail could not be read", e);
|
||||||
|
|
|
@ -36,11 +36,12 @@ public final class GroupManager {
|
||||||
private static final String TAG = Log.tag(GroupManager.class);
|
private static final String TAG = Log.tag(GroupManager.class);
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static @NonNull GroupActionResult createGroup(@NonNull Context context,
|
public static @NonNull GroupActionResult createGroup(@NonNull Context context,
|
||||||
@NonNull Set<Recipient> members,
|
@NonNull Set<Recipient> members,
|
||||||
@Nullable byte[] avatar,
|
@Nullable byte[] avatar,
|
||||||
@Nullable String name,
|
@Nullable String name,
|
||||||
boolean mms)
|
boolean mms,
|
||||||
|
int disappearingMessagesTimer)
|
||||||
throws GroupChangeBusyException, GroupChangeFailedException, IOException
|
throws GroupChangeBusyException, GroupChangeFailedException, IOException
|
||||||
{
|
{
|
||||||
boolean shouldAttemptToCreateV2 = !mms && !SignalStore.internalValues().gv2DoNotCreateGv2Groups();
|
boolean shouldAttemptToCreateV2 = !mms && !SignalStore.internalValues().gv2DoNotCreateGv2Groups();
|
||||||
|
@ -49,7 +50,7 @@ public final class GroupManager {
|
||||||
if (shouldAttemptToCreateV2) {
|
if (shouldAttemptToCreateV2) {
|
||||||
try {
|
try {
|
||||||
try (GroupManagerV2.GroupCreator groupCreator = new GroupManagerV2(context).create()) {
|
try (GroupManagerV2.GroupCreator groupCreator = new GroupManagerV2(context).create()) {
|
||||||
return groupCreator.createGroup(memberIds, name, avatar);
|
return groupCreator.createGroup(memberIds, name, avatar, disappearingMessagesTimer);
|
||||||
}
|
}
|
||||||
} catch (MembershipNotSuitableForV2Exception e) {
|
} catch (MembershipNotSuitableForV2Exception e) {
|
||||||
Log.w(TAG, "Attempted to make a GV2, but membership was not suitable, falling back to GV1", e);
|
Log.w(TAG, "Attempted to make a GV2, but membership was not suitable, falling back to GV1", e);
|
||||||
|
|
|
@ -244,23 +244,15 @@ final class GroupManagerV2 {
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
@NonNull GroupManager.GroupActionResult createGroup(@NonNull Collection<RecipientId> members,
|
@NonNull GroupManager.GroupActionResult createGroup(@NonNull Collection<RecipientId> members,
|
||||||
@Nullable String name,
|
@Nullable String name,
|
||||||
@Nullable byte[] avatar)
|
@Nullable byte[] avatar,
|
||||||
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
|
int disappearingMessagesTimer)
|
||||||
{
|
|
||||||
return createGroup(name, avatar, members);
|
|
||||||
}
|
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
private @NonNull GroupManager.GroupActionResult createGroup(@Nullable String name,
|
|
||||||
@Nullable byte[] avatar,
|
|
||||||
@NonNull Collection<RecipientId> members)
|
|
||||||
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
|
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
|
||||||
{
|
{
|
||||||
GroupSecretParams groupSecretParams = GroupSecretParams.generate();
|
GroupSecretParams groupSecretParams = GroupSecretParams.generate();
|
||||||
DecryptedGroup decryptedGroup;
|
DecryptedGroup decryptedGroup;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, Member.Role.DEFAULT, 0);
|
decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, Member.Role.DEFAULT, disappearingMessagesTimer);
|
||||||
} catch (GroupAlreadyExistsException e) {
|
} catch (GroupAlreadyExistsException e) {
|
||||||
throw new GroupChangeFailedException(e);
|
throw new GroupChangeFailedException(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ public enum GroupChangeFailureReason {
|
||||||
NETWORK,
|
NETWORK,
|
||||||
OTHER;
|
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 MembershipNotSuitableForV2Exception) return GroupChangeFailureReason.NOT_CAPABLE;
|
||||||
if (e instanceof IOException) return GroupChangeFailureReason.NETWORK;
|
if (e instanceof IOException) return GroupChangeFailureReason.NETWORK;
|
||||||
if (e instanceof GroupNotAMemberException) return GroupChangeFailureReason.NOT_A_MEMBER;
|
if (e instanceof GroupNotAMemberException) return GroupChangeFailureReason.NOT_A_MEMBER;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
@ -30,8 +31,10 @@ import com.dd.CircularProgressButton;
|
||||||
import org.signal.core.util.EditTextUtil;
|
import org.signal.core.util.EditTextUtil;
|
||||||
import org.thoughtcrime.securesms.LoggingFragment;
|
import org.thoughtcrime.securesms.LoggingFragment;
|
||||||
import org.thoughtcrime.securesms.R;
|
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.GroupMemberListView;
|
||||||
import org.thoughtcrime.securesms.groups.ui.creategroup.dialogs.NonGv2MemberDialog;
|
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.AvatarSelectionActivity;
|
||||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
|
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
|
||||||
import org.thoughtcrime.securesms.mediasend.Media;
|
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.profiles.AvatarHelper;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
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.BitmapUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
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 int AVATAR_PLACEHOLDER_INSET_DP = 18;
|
||||||
private static final short REQUEST_CODE_AVATAR = 27621;
|
private static final short REQUEST_CODE_AVATAR = 27621;
|
||||||
|
private static final short REQUEST_DISAPPEARING_TIMER = 28621;
|
||||||
|
|
||||||
private CircularProgressButton create;
|
private CircularProgressButton create;
|
||||||
private Callback callback;
|
private Callback callback;
|
||||||
|
@ -61,6 +67,7 @@ public class AddGroupDetailsFragment extends LoggingFragment {
|
||||||
private Drawable avatarPlaceholder;
|
private Drawable avatarPlaceholder;
|
||||||
private EditText name;
|
private EditText name;
|
||||||
private Toolbar toolbar;
|
private Toolbar toolbar;
|
||||||
|
private View disappearingMessagesRow;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(@NonNull Context context) {
|
public void onAttach(@NonNull Context context) {
|
||||||
|
@ -83,17 +90,19 @@ public class AddGroupDetailsFragment extends LoggingFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
create = view.findViewById(R.id.create);
|
create = view.findViewById(R.id.create);
|
||||||
name = view.findViewById(R.id.name);
|
name = view.findViewById(R.id.name);
|
||||||
toolbar = view.findViewById(R.id.toolbar);
|
toolbar = view.findViewById(R.id.toolbar);
|
||||||
|
disappearingMessagesRow = view.findViewById(R.id.group_disappearing_messages_row);
|
||||||
|
|
||||||
setCreateEnabled(false, false);
|
setCreateEnabled(false, false);
|
||||||
|
|
||||||
GroupMemberListView members = view.findViewById(R.id.member_list);
|
GroupMemberListView members = view.findViewById(R.id.member_list);
|
||||||
ImageView avatar = view.findViewById(R.id.group_avatar);
|
ImageView avatar = view.findViewById(R.id.group_avatar);
|
||||||
View mmsWarning = view.findViewById(R.id.mms_warning);
|
View mmsWarning = view.findViewById(R.id.mms_warning);
|
||||||
LearnMoreTextView gv2Warning = view.findViewById(R.id.gv2_warning);
|
LearnMoreTextView gv2Warning = view.findViewById(R.id.gv2_warning);
|
||||||
View addLater = view.findViewById(R.id.add_later);
|
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());
|
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.getCanSubmitForm().observe(getViewLifecycleOwner(), isFormValid -> setCreateEnabled(isFormValid, true));
|
||||||
viewModel.getIsMms().observe(getViewLifecycleOwner(), isMms -> {
|
viewModel.getIsMms().observe(getViewLifecycleOwner(), isMms -> {
|
||||||
|
disappearingMessagesRow.setVisibility(isMms ? View.GONE : View.VISIBLE);
|
||||||
mmsWarning.setVisibility(isMms ? View.VISIBLE : View.GONE);
|
mmsWarning.setVisibility(isMms ? View.VISIBLE : View.GONE);
|
||||||
name.setHint(isMms ? R.string.AddGroupDetailsFragment__group_name_optional : R.string.AddGroupDetailsFragment__group_name_required);
|
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);
|
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();
|
name.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,6 +190,8 @@ public class AddGroupDetailsFragment extends LoggingFragment {
|
||||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
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 {
|
} else {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.groups.GroupChangeException;
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||||
import org.thoughtcrime.securesms.groups.GroupsV2CapabilityChecker;
|
import org.thoughtcrime.securesms.groups.GroupsV2CapabilityChecker;
|
||||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
|
||||||
|
@ -48,17 +49,24 @@ final class AddGroupDetailsRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void createGroup(@NonNull Set<RecipientId> members,
|
void createGroup(@NonNull Set<RecipientId> members,
|
||||||
@Nullable byte[] avatar,
|
@Nullable byte[] avatar,
|
||||||
@Nullable String name,
|
@Nullable String name,
|
||||||
boolean mms,
|
boolean mms,
|
||||||
|
@Nullable Integer disappearingMessagesTimer,
|
||||||
Consumer<GroupCreateResult> resultConsumer)
|
Consumer<GroupCreateResult> resultConsumer)
|
||||||
{
|
{
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
Set<Recipient> recipients = new HashSet<>(Stream.of(members).map(Recipient::resolved).toList());
|
Set<Recipient> recipients = new HashSet<>(Stream.of(members).map(Recipient::resolved).toList());
|
||||||
|
|
||||||
try {
|
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));
|
resultConsumer.accept(GroupCreateResult.success(result));
|
||||||
} catch (GroupChangeBusyException e) {
|
} catch (GroupChangeBusyException e) {
|
||||||
|
|
|
@ -32,10 +32,11 @@ import java.util.Set;
|
||||||
public final class AddGroupDetailsViewModel extends ViewModel {
|
public final class AddGroupDetailsViewModel extends ViewModel {
|
||||||
|
|
||||||
private final LiveData<List<GroupMemberEntry.NewGroupCandidate>> members;
|
private final LiveData<List<GroupMemberEntry.NewGroupCandidate>> members;
|
||||||
private final DefaultValueLiveData<Set<RecipientId>> deleted = new DefaultValueLiveData<>(new HashSet<>());
|
private final DefaultValueLiveData<Set<RecipientId>> deleted = new DefaultValueLiveData<>(new HashSet<>());
|
||||||
private final MutableLiveData<String> name = new MutableLiveData<>("");
|
private final MutableLiveData<String> name = new MutableLiveData<>("");
|
||||||
private final MutableLiveData<byte[]> avatar = new MutableLiveData<>();
|
private final MutableLiveData<byte[]> avatar = new MutableLiveData<>();
|
||||||
private final SingleLiveEvent<GroupCreateResult> groupCreateResult = new SingleLiveEvent<>();
|
private final SingleLiveEvent<GroupCreateResult> groupCreateResult = new SingleLiveEvent<>();
|
||||||
|
private final MutableLiveData<Integer> disappearingMessagesTimer = new MutableLiveData<>(SignalStore.settings().getUniversalExpireTimer());
|
||||||
private final LiveData<Boolean> isMms;
|
private final LiveData<Boolean> isMms;
|
||||||
private final LiveData<Boolean> canSubmitForm;
|
private final LiveData<Boolean> canSubmitForm;
|
||||||
private final AddGroupDetailsRepository repository;
|
private final AddGroupDetailsRepository repository;
|
||||||
|
@ -47,12 +48,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
|
|
||||||
MutableLiveData<List<GroupMemberEntry.NewGroupCandidate>> initialMembers = new MutableLiveData<>();
|
MutableLiveData<List<GroupMemberEntry.NewGroupCandidate>> initialMembers = new MutableLiveData<>();
|
||||||
|
LiveData<Boolean> isValidName = Transformations.map(name, name -> !TextUtils.isEmpty(name));
|
||||||
|
|
||||||
LiveData<Boolean> 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<List<GroupMemberEntry.NewGroupCandidate>> membersToCheckGv2CapabilityOf = LiveDataUtil.combineLatest(isMms, members, (forcedMms, memberList) -> {
|
LiveData<List<GroupMemberEntry.NewGroupCandidate>> membersToCheckGv2CapabilityOf = LiveDataUtil.combineLatest(isMms, members, (forcedMms, memberList) -> {
|
||||||
if (SignalStore.internalValues().gv2DoNotCreateGv2Groups() || forcedMms) {
|
if (SignalStore.internalValues().gv2DoNotCreateGv2Groups() || forcedMms) {
|
||||||
|
@ -94,6 +93,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
|
||||||
return nonGv2CapableMembers;
|
return nonGv2CapableMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull LiveData<Integer> getDisappearingMessagesTimer() {
|
||||||
|
return disappearingMessagesTimer;
|
||||||
|
}
|
||||||
|
|
||||||
void setAvatar(@Nullable byte[] avatar) {
|
void setAvatar(@Nullable byte[] avatar) {
|
||||||
this.avatar.setValue(avatar);
|
this.avatar.setValue(avatar);
|
||||||
}
|
}
|
||||||
|
@ -107,18 +110,19 @@ public final class AddGroupDetailsViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
void delete(@NonNull RecipientId recipientId) {
|
void delete(@NonNull RecipientId recipientId) {
|
||||||
Set<RecipientId> deleted = this.deleted.getValue();
|
Set<RecipientId> deleted = this.deleted.getValue();
|
||||||
|
|
||||||
deleted.add(recipientId);
|
deleted.add(recipientId);
|
||||||
this.deleted.setValue(deleted);
|
this.deleted.setValue(deleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
void create() {
|
void create() {
|
||||||
List<GroupMemberEntry.NewGroupCandidate> members = Objects.requireNonNull(this.members.getValue());
|
List<GroupMemberEntry.NewGroupCandidate> members = Objects.requireNonNull(this.members.getValue());
|
||||||
Set<RecipientId> memberIds = Stream.of(members).map(member -> member.getMember().getId()).collect(Collectors.toSet());
|
Set<RecipientId> memberIds = Stream.of(members).map(member -> member.getMember().getId()).collect(Collectors.toSet());
|
||||||
byte[] avatarBytes = avatar.getValue();
|
byte[] avatarBytes = avatar.getValue();
|
||||||
boolean isGroupMms = isMms.getValue() == Boolean.TRUE;
|
boolean isGroupMms = isMms.getValue() == Boolean.TRUE;
|
||||||
String groupName = name.getValue();
|
String groupName = name.getValue();
|
||||||
|
Integer disappearingTimer = disappearingMessagesTimer.getValue();
|
||||||
|
|
||||||
if (!isGroupMms && TextUtils.isEmpty(groupName)) {
|
if (!isGroupMms && TextUtils.isEmpty(groupName)) {
|
||||||
groupCreateResult.postValue(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_INVALID_NAME));
|
groupCreateResult.postValue(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_INVALID_NAME));
|
||||||
|
@ -129,6 +133,7 @@ public final class AddGroupDetailsViewModel extends ViewModel {
|
||||||
avatarBytes,
|
avatarBytes,
|
||||||
groupName,
|
groupName,
|
||||||
isGroupMms,
|
isGroupMms,
|
||||||
|
disappearingTimer,
|
||||||
groupCreateResult::postValue);
|
groupCreateResult::postValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +148,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
|
||||||
.anyMatch(member -> !member.getMember().isRegistered());
|
.anyMatch(member -> !member.getMember().isRegistered());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDisappearingMessageTimer(int timer) {
|
||||||
|
disappearingMessagesTimer.setValue(timer);
|
||||||
|
}
|
||||||
|
|
||||||
static final class Factory implements ViewModelProvider.Factory {
|
static final class Factory implements ViewModelProvider.Factory {
|
||||||
|
|
||||||
private final Collection<RecipientId> recipientIds;
|
private final Collection<RecipientId> recipientIds;
|
||||||
|
|
|
@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
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.notifications.CustomNotificationsDialogFragment;
|
||||||
import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroupLinkDialogFragment;
|
import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroupLinkDialogFragment;
|
||||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||||
|
@ -280,7 +281,12 @@ public class ManageGroupFragment extends LoggingFragment {
|
||||||
|
|
||||||
viewModel.getDisappearingMessageTimer().observe(getViewLifecycleOwner(), string -> disappearingMessages.setText(string));
|
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()));
|
blockGroup.setOnClickListener(v -> viewModel.blockAndLeave(requireActivity()));
|
||||||
unblockGroup.setOnClickListener(v -> viewModel.unblock(requireActivity()));
|
unblockGroup.setOnClickListener(v -> viewModel.unblock(requireActivity()));
|
||||||
|
|
||||||
|
|
|
@ -79,17 +79,6 @@ final class ManageGroupRepository {
|
||||||
return new GroupStateResult(threadId, groupRecipient);
|
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) {
|
void applyMembershipRightsChange(@NonNull GroupId groupId, @NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) {
|
||||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -257,14 +257,6 @@ public class ManageGroupViewModel extends ViewModel {
|
||||||
return groupInfoMessage;
|
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) {
|
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights) {
|
||||||
manageGroupRepository.applyMembershipRightsChange(getGroupId(), newRights, this::showErrorToast);
|
manageGroupRepository.applyMembershipRightsChange(getGroupId(), newRights, this::showErrorToast);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration;
|
||||||
import org.thoughtcrime.securesms.jobmanager.migrations.RetrieveProfileJobMigration;
|
import org.thoughtcrime.securesms.jobmanager.migrations.RetrieveProfileJobMigration;
|
||||||
import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration;
|
import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration;
|
||||||
import org.thoughtcrime.securesms.migrations.AccountRecordMigrationJob;
|
import org.thoughtcrime.securesms.migrations.AccountRecordMigrationJob;
|
||||||
|
import org.thoughtcrime.securesms.migrations.ApplyUnknownFieldsToSelfMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.AttributesMigrationJob;
|
import org.thoughtcrime.securesms.migrations.AttributesMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
|
import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
|
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
|
||||||
|
@ -160,6 +161,7 @@ public final class JobManagerFactories {
|
||||||
|
|
||||||
// Migrations
|
// Migrations
|
||||||
put(AccountRecordMigrationJob.KEY, new AccountRecordMigrationJob.Factory());
|
put(AccountRecordMigrationJob.KEY, new AccountRecordMigrationJob.Factory());
|
||||||
|
put(ApplyUnknownFieldsToSelfMigrationJob.KEY, new ApplyUnknownFieldsToSelfMigrationJob.Factory());
|
||||||
put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory());
|
put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory());
|
||||||
put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory());
|
put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory());
|
||||||
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
|
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
|
||||||
|
|
|
@ -9,15 +9,15 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.LiveData;
|
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.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
|
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.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
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 org.thoughtcrime.securesms.webrtc.CallBandwidthMode;
|
||||||
|
|
||||||
import java.util.Arrays;
|
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_LENGTH = "pref_trim_length";
|
||||||
public static final String THREAD_TRIM_ENABLED = "pref_trim_threads";
|
public static final String THREAD_TRIM_ENABLED = "pref_trim_threads";
|
||||||
|
|
||||||
public static final String THEME = "settings.theme";
|
public static final String THEME = "settings.theme";
|
||||||
public static final String MESSAGE_FONT_SIZE = "settings.message.font.size";
|
public static final String MESSAGE_FONT_SIZE = "settings.message.font.size";
|
||||||
public static final String LANGUAGE = "settings.language";
|
public static final String LANGUAGE = "settings.language";
|
||||||
public static final String PREFER_SYSTEM_EMOJI = "settings.use.system.emoji";
|
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 ENTER_KEY_SENDS = "settings.enter.key.sends";
|
||||||
public static final String BACKUPS_ENABLED = "settings.backups.enabled";
|
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 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 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_NOTIFICATIONS_ENABLED = "settings.message.notifications.enabled";
|
||||||
public static final String MESSAGE_NOTIFICATION_SOUND = "settings.message.notifications.sound";
|
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_VIBRATE_ENABLED = "settings.message.vibrate.enabled";
|
||||||
public static final String MESSAGE_LED_COLOR = "settings.message.led.color";
|
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_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_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_REPEAT_ALERTS = "settings.message.repeat.alerts";
|
||||||
public static final String MESSAGE_NOTIFICATION_PRIVACY = "settings.message.notification.privacy";
|
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_NOTIFICATIONS_ENABLED = "settings.call.notifications.enabled";
|
||||||
public static final String CALL_RINGTONE = "settings.call.ringtone";
|
public static final String CALL_RINGTONE = "settings.call.ringtone";
|
||||||
public static final String CALL_VIBRATE_ENABLED = "settings.call.vibrate.enabled";
|
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 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<String> onConfigurationSettingChanged = new SingleLiveEvent<>();
|
private final SingleLiveEvent<String> onConfigurationSettingChanged = new SingleLiveEvent<>();
|
||||||
|
|
||||||
private static final String DEFAULT_SMS = "settings.default_sms";
|
|
||||||
|
|
||||||
SettingsValues(@NonNull KeyValueStore store) {
|
SettingsValues(@NonNull KeyValueStore store) {
|
||||||
super(store);
|
super(store);
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,8 @@ public final class SettingsValues extends SignalStoreValues {
|
||||||
CALL_NOTIFICATIONS_ENABLED,
|
CALL_NOTIFICATIONS_ENABLED,
|
||||||
CALL_RINGTONE,
|
CALL_RINGTONE,
|
||||||
CALL_VIBRATE_ENABLED,
|
CALL_VIBRATE_ENABLED,
|
||||||
NOTIFY_WHEN_CONTACT_JOINS_SIGNAL);
|
NOTIFY_WHEN_CONTACT_JOINS_SIGNAL,
|
||||||
|
UNIVERSAL_EXPIRE_TIMER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull LiveData<String> getOnConfigurationSettingChanged() {
|
public @NonNull LiveData<String> 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) {
|
private @Nullable Uri getUri(@NonNull String key) {
|
||||||
String uri = getString(key, "");
|
String uri = getString(key, "");
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ public class ApplicationMigrations {
|
||||||
|
|
||||||
private static final int LEGACY_CANONICAL_VERSION = 455;
|
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 {
|
private static final class Version {
|
||||||
static final int LEGACY = 1;
|
static final int LEGACY = 1;
|
||||||
|
@ -75,6 +75,7 @@ public class ApplicationMigrations {
|
||||||
static final int MUTE_SYNC = 31;
|
static final int MUTE_SYNC = 31;
|
||||||
static final int PROFILE_SHARING_UPDATE = 32;
|
static final int PROFILE_SHARING_UPDATE = 32;
|
||||||
static final int SMS_STORAGE_SYNC = 33;
|
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());
|
jobs.put(Version.SMS_STORAGE_SYNC, new AccountRecordMigrationJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastSeenVersion < Version.APPLY_UNIVERSAL_EXPIRE) {
|
||||||
|
jobs.put(Version.SMS_STORAGE_SYNC, new ApplyUnknownFieldsToSelfMigrationJob());
|
||||||
|
}
|
||||||
|
|
||||||
return jobs;
|
return jobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<ApplyUnknownFieldsToSelfMigrationJob> {
|
||||||
|
@Override
|
||||||
|
public @NonNull ApplyUnknownFieldsToSelfMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||||
|
return new ApplyUnknownFieldsToSelfMigrationJob(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,6 +78,23 @@ public class OutgoingMediaMessage {
|
||||||
contacts, linkPreviews, mentions, new LinkedList<>(), new LinkedList<>());
|
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) {
|
public OutgoingMediaMessage(OutgoingMediaMessage that) {
|
||||||
this.recipient = that.getRecipient();
|
this.recipient = that.getRecipient();
|
||||||
this.body = that.body;
|
this.body = that.body;
|
||||||
|
|
|
@ -23,6 +23,8 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
|
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
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.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
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
|
@WorkerThread
|
||||||
private static boolean isMessageRequestAccepted(@NonNull Context context, long threadId, @NonNull Recipient threadRecipient) {
|
private static boolean isMessageRequestAccepted(@NonNull Context context, long threadId, @NonNull Recipient threadRecipient) {
|
||||||
return threadRecipient.isSelf() ||
|
return threadRecipient.isSelf() ||
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
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.recipients.ui.notifications.CustomNotificationsDialogFragment;
|
||||||
import org.thoughtcrime.securesms.util.DateUtils;
|
import org.thoughtcrime.securesms.util.DateUtils;
|
||||||
import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
|
import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
|
||||||
|
@ -239,7 +240,7 @@ public class ManageRecipientFragment extends LoggingFragment {
|
||||||
internalDetails.setVisibility(View.GONE);
|
internalDetails.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
disappearingMessagesRow.setOnClickListener(v -> viewModel.handleExpirationSelection(requireContext()));
|
disappearingMessagesRow.setOnClickListener(v -> startActivity(RecipientDisappearingMessagesActivity.forRecipient(requireContext(), recipientId)));
|
||||||
block.setOnClickListener(v -> viewModel.onBlockClicked(requireActivity()));
|
block.setOnClickListener(v -> viewModel.onBlockClicked(requireActivity()));
|
||||||
unblock.setOnClickListener(v -> viewModel.onUnblockClicked(requireActivity()));
|
unblock.setOnClickListener(v -> viewModel.onUnblockClicked(requireActivity()));
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,8 @@ import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -62,14 +60,6 @@ final class ManageRecipientRepository {
|
||||||
.orNull()));
|
.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<List<RecipientId>> onComplete) {
|
void getGroupMembership(@NonNull Consumer<List<RecipientId>> onComplete) {
|
||||||
SignalExecutors.BOUNDED.execute(() -> {
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||||
|
|
|
@ -187,13 +187,6 @@ public final class ManageRecipientViewModel extends ViewModel {
|
||||||
return canUnblock;
|
return canUnblock;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleExpirationSelection(@NonNull Context context) {
|
|
||||||
withRecipient(recipient ->
|
|
||||||
ExpirationDialog.show(context,
|
|
||||||
recipient.getExpireMessages(),
|
|
||||||
manageRecipientRepository::setExpiration));
|
|
||||||
}
|
|
||||||
|
|
||||||
void setMuteUntil(long muteUntil) {
|
void setMuteUntil(long muteUntil) {
|
||||||
manageRecipientRepository.setMuteUntil(muteUntil);
|
manageRecipientRepository.setMuteUntil(muteUntil);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
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.ringrtc.RemotePeer;
|
||||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata;
|
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata;
|
||||||
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.OfferMetadata;
|
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.OfferMetadata;
|
||||||
|
@ -76,6 +78,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||||
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, remotePeer);
|
webRtcInteractor.setCallInProgressNotification(TYPE_OUTGOING_RINGING, remotePeer);
|
||||||
webRtcInteractor.setWantsBluetoothConnection(true);
|
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());
|
DatabaseFactory.getSmsDatabase(context).insertOutgoingCall(remotePeer.getId(), currentState.getCallSetupState().isEnableVideoOnCreate());
|
||||||
|
|
||||||
webRtcInteractor.retrieveTurnServers(remotePeer);
|
webRtcInteractor.retrieveTurnServers(remotePeer);
|
||||||
|
@ -97,6 +100,8 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||||
Integer destinationDeviceId = broadcast ? null : callMetadata.getRemoteDevice();
|
Integer destinationDeviceId = broadcast ? null : callMetadata.getRemoteDevice();
|
||||||
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOffer(offerMessage, true, destinationDeviceId);
|
SignalServiceCallMessage callMessage = SignalServiceCallMessage.forOffer(offerMessage, true, destinationDeviceId);
|
||||||
|
|
||||||
|
Recipient callRecipient = currentState.getCallInfoState().getCallRecipient();
|
||||||
|
RecipientUtil.shareProfileIfFirstSecureMessage(context, callRecipient);
|
||||||
webRtcInteractor.sendCallMessage(callMetadata.getRemotePeer(), callMessage);
|
webRtcInteractor.sendCallMessage(callMetadata.getRemotePeer(), callMessage);
|
||||||
|
|
||||||
return currentState;
|
return currentState;
|
||||||
|
|
|
@ -28,7 +28,6 @@ import com.annimon.stream.Stream;
|
||||||
|
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.ApplicationContext;
|
|
||||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
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.RemoteDeleteSendJob;
|
||||||
import org.thoughtcrime.securesms.jobs.ResumableUploadSpecJob;
|
import org.thoughtcrime.securesms.jobs.ResumableUploadSpecJob;
|
||||||
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
@ -106,7 +107,11 @@ public class MessageSender {
|
||||||
boolean keyExchange = message.isKeyExchange();
|
boolean keyExchange = message.isKeyExchange();
|
||||||
|
|
||||||
long allocatedThreadId = DatabaseFactory.getThreadDatabase(context).getOrCreateValidThreadId(recipient, threadId);
|
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);
|
sendTextMessage(context, recipient, forceSms, keyExchange, messageId);
|
||||||
onMessageSent();
|
onMessageSent();
|
||||||
|
@ -127,7 +132,7 @@ public class MessageSender {
|
||||||
|
|
||||||
long allocatedThreadId = threadDatabase.getOrCreateValidThreadId(message.getRecipient(), threadId, message.getDistributionType());
|
long allocatedThreadId = threadDatabase.getOrCreateValidThreadId(message.getRecipient(), threadId, message.getDistributionType());
|
||||||
Recipient recipient = message.getRecipient();
|
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());
|
sendMediaMessage(context, recipient, forceSms, messageId, Collections.emptyList());
|
||||||
onMessageSent();
|
onMessageSent();
|
||||||
|
@ -163,7 +168,10 @@ public class MessageSender {
|
||||||
}
|
}
|
||||||
|
|
||||||
Recipient recipient = message.getRecipient();
|
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<AttachmentId> attachmentIds = Stream.of(preUploadResults).map(PreUploadResult::getAttachmentId).toList();
|
List<AttachmentId> attachmentIds = Stream.of(preUploadResults).map(PreUploadResult::getAttachmentId).toList();
|
||||||
List<String> jobIds = Stream.of(preUploadResults).map(PreUploadResult::getJobIds).flatMap(Stream::of).toList();
|
List<String> jobIds = Stream.of(preUploadResults).map(PreUploadResult::getJobIds).flatMap(Stream::of).toList();
|
||||||
|
@ -198,7 +206,10 @@ public class MessageSender {
|
||||||
try {
|
try {
|
||||||
OutgoingSecureMediaMessage primaryMessage = messages.get(0);
|
OutgoingSecureMediaMessage primaryMessage = messages.get(0);
|
||||||
long primaryThreadId = threadDatabase.getThreadIdFor(primaryMessage.getRecipient(), primaryMessage.getDistributionType());
|
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);
|
attachmentDatabase.updateMessageId(preUploadAttachmentIds, primaryMessageId);
|
||||||
messageIds.add(primaryMessageId);
|
messageIds.add(primaryMessageId);
|
||||||
|
@ -216,7 +227,10 @@ public class MessageSender {
|
||||||
|
|
||||||
for (OutgoingSecureMediaMessage secondaryMessage : secondaryMessages) {
|
for (OutgoingSecureMediaMessage secondaryMessage : secondaryMessages) {
|
||||||
long allocatedThreadId = threadDatabase.getThreadIdFor(secondaryMessage.getRecipient(), secondaryMessage.getDistributionType());
|
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<AttachmentId> attachmentIds = new ArrayList<>(preUploadAttachmentIds.size());
|
List<AttachmentId> attachmentIds = new ArrayList<>(preUploadAttachmentIds.size());
|
||||||
|
|
||||||
for (int i = 0; i < preUploadAttachments.size(); i++) {
|
for (int i = 0; i < preUploadAttachments.size(); i++) {
|
||||||
|
@ -355,6 +369,20 @@ public class MessageSender {
|
||||||
EventBus.getDefault().postSticky(MessageSentEvent.INSTANCE);
|
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<String> uploadJobIds)
|
private static void sendMediaMessage(Context context, Recipient recipient, boolean forceSms, long messageId, @NonNull Collection<String> uploadJobIds)
|
||||||
{
|
{
|
||||||
if (isLocalSelfSend(context, recipient, forceSms)) {
|
if (isLocalSelfSend(context, recipient, forceSms)) {
|
||||||
|
|
|
@ -28,6 +28,10 @@ public class OutgoingTextMessage {
|
||||||
this.message = body;
|
this.message = body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OutgoingTextMessage(OutgoingTextMessage base, long expiresIn) {
|
||||||
|
this(base.getRecipient(), base.getMessageBody(), expiresIn, base.getSubscriptionId());
|
||||||
|
}
|
||||||
|
|
||||||
public long getExpiresIn() {
|
public long getExpiresIn() {
|
||||||
return expiresIn;
|
return expiresIn;
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,9 +97,10 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||||
List<PinnedConversation> pinnedConversations = remote.getPinnedConversations();
|
List<PinnedConversation> pinnedConversations = remote.getPinnedConversations();
|
||||||
AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode = remote.getPhoneNumberSharingMode();
|
AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode = remote.getPhoneNumberSharingMode();
|
||||||
boolean preferContactAvatars = remote.isPreferContactAvatars();
|
boolean preferContactAvatars = remote.isPreferContactAvatars();
|
||||||
|
int universalExpireTimer = remote.getUniversalExpireTimer();
|
||||||
boolean primarySendsSms = local.isPrimarySendsSms();
|
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 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, 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) {
|
if (matchesRemote) {
|
||||||
return remote;
|
return remote;
|
||||||
|
@ -124,6 +125,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||||
.setPinnedConversations(pinnedConversations)
|
.setPinnedConversations(pinnedConversations)
|
||||||
.setPreferContactAvatars(preferContactAvatars)
|
.setPreferContactAvatars(preferContactAvatars)
|
||||||
.setPayments(payments.isEnabled(), payments.getEntropy().orNull())
|
.setPayments(payments.isEnabled(), payments.getEntropy().orNull())
|
||||||
|
.setUniversalExpireTimer(universalExpireTimer)
|
||||||
.setPrimarySendsSms(primarySendsSms)
|
.setPrimarySendsSms(primarySendsSms)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
@ -161,6 +163,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||||
@NonNull List<PinnedConversation> pinnedConversations,
|
@NonNull List<PinnedConversation> pinnedConversations,
|
||||||
boolean preferContactAvatars,
|
boolean preferContactAvatars,
|
||||||
SignalAccountRecord.Payments payments,
|
SignalAccountRecord.Payments payments,
|
||||||
|
int universalExpireTimer,
|
||||||
boolean primarySendsSms)
|
boolean primarySendsSms)
|
||||||
{
|
{
|
||||||
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
|
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
|
||||||
|
@ -178,6 +181,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
||||||
contact.getPhoneNumberSharingMode() == phoneNumberSharingMode &&
|
contact.getPhoneNumberSharingMode() == phoneNumberSharingMode &&
|
||||||
contact.isPhoneNumberUnlisted() == unlistedPhoneNumber &&
|
contact.isPhoneNumberUnlisted() == unlistedPhoneNumber &&
|
||||||
contact.isPreferContactAvatars() == preferContactAvatars &&
|
contact.isPreferContactAvatars() == preferContactAvatars &&
|
||||||
|
contact.getUniversalExpireTimer() == universalExpireTimer &&
|
||||||
contact.isPrimarySendsSms() == primarySendsSms &&
|
contact.isPrimarySendsSms() == primarySendsSms &&
|
||||||
Objects.equals(contact.getPinnedConversations(), pinnedConversations);
|
Objects.equals(contact.getPinnedConversations(), pinnedConversations);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.payments.Entropy;
|
import org.thoughtcrime.securesms.payments.Entropy;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
import org.thoughtcrime.securesms.util.SetUtil;
|
import org.thoughtcrime.securesms.util.SetUtil;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
@ -32,14 +31,8 @@ import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -156,6 +149,7 @@ public final class StorageSyncHelper {
|
||||||
SignalStore.phoneNumberPrivacy().setPhoneNumberSharingMode(StorageSyncModels.remoteToLocalPhoneNumberSharingMode(update.getNew().getPhoneNumberSharingMode()));
|
SignalStore.phoneNumberPrivacy().setPhoneNumberSharingMode(StorageSyncModels.remoteToLocalPhoneNumberSharingMode(update.getNew().getPhoneNumberSharingMode()));
|
||||||
SignalStore.settings().setPreferSystemContactPhotos(update.getNew().isPreferContactAvatars());
|
SignalStore.settings().setPreferSystemContactPhotos(update.getNew().isPreferContactAvatars());
|
||||||
SignalStore.paymentsValues().setEnabledAndEntropy(update.getNew().getPayments().isEnabled(), Entropy.fromBytes(update.getNew().getPayments().getEntropy().orNull()));
|
SignalStore.paymentsValues().setEnabledAndEntropy(update.getNew().getPayments().isEnabled(), Entropy.fromBytes(update.getNew().getPayments().getEntropy().orNull()));
|
||||||
|
SignalStore.settings().setUniversalExpireTimer(update.getNew().getUniversalExpireTimer());
|
||||||
|
|
||||||
if (fetchProfile && update.getNew().getAvatarUrlPath().isPresent()) {
|
if (fetchProfile && update.getNew().getAvatarUrlPath().isPresent()) {
|
||||||
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(self, update.getNew().getAvatarUrlPath().get()));
|
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(self, update.getNew().getAvatarUrlPath().get()));
|
||||||
|
|
|
@ -151,6 +151,10 @@ public final class SqlUtil {
|
||||||
return new Query(column + " IN (" + query.toString() + ")", buildArgs(args));
|
return new Query(column + " IN (" + query.toString() + ")", buildArgs(args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @NonNull Query buildQuery(@NonNull String where, @NonNull Object... args) {
|
||||||
|
return new SqlUtil.Query(where, SqlUtil.buildArgs(args));
|
||||||
|
}
|
||||||
|
|
||||||
public static String[] appendArg(@NonNull String[] args, String addition) {
|
public static String[] appendArg(@NonNull String[] args, String addition) {
|
||||||
String[] output = new String[args.length + 1];
|
String[] output = new String[args.length + 1];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package org.thoughtcrime.securesms.util.livedata
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
|
||||||
|
fun <T, R> LiveData<T>.distinctUntilChanged(selector: (T) -> R): LiveData<T> {
|
||||||
|
return LiveDataUtil.distinctUntilChanged(this, selector)
|
||||||
|
}
|
|
@ -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<T> {
|
||||||
|
class Idle<T> : ProcessState<T>()
|
||||||
|
class Working<T> : ProcessState<T>()
|
||||||
|
data class Success<T>(val result: T) : ProcessState<T>()
|
||||||
|
data class Failure<T>(val throwable: Throwable?) : ProcessState<T>()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T> fromResult(result: Result<T>): ProcessState<T> {
|
||||||
|
return if (result.isSuccess) {
|
||||||
|
Success(result.getOrThrow())
|
||||||
|
} else {
|
||||||
|
Failure(result.exceptionOrNull())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,6 +39,42 @@
|
||||||
app:layout_constraintStart_toEndOf="@id/group_avatar"
|
app:layout_constraintStart_toEndOf="@id/group_avatar"
|
||||||
app:layout_constraintTop_toTopOf="@id/group_avatar" />
|
app:layout_constraintTop_toTopOf="@id/group_avatar" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/group_disappearing_messages_row"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/group_avatar">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:srcCompat="@drawable/ic_timer_disabled_24"
|
||||||
|
app:tint="@color/signal_text_primary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/PrivacySettingsFragment__disappearing_messages"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/group_disappearing_messages_value"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
android:textColor="@color/signal_text_secondary"
|
||||||
|
tools:text="1 week" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/mms_warning"
|
android:id="@+id/mms_warning"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -47,7 +83,7 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toTopOf="@id/gv2_warning"
|
app:layout_constraintBottom_toTopOf="@id/gv2_warning"
|
||||||
app:layout_constraintTop_toBottomOf="@id/group_avatar"
|
app:layout_constraintTop_toBottomOf="@id/group_disappearing_messages_row"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|
|
@ -25,11 +25,6 @@
|
||||||
android:paddingBottom="2dp"
|
android:paddingBottom="2dp"
|
||||||
android:scrollbars="vertical" />
|
android:scrollbars="vertical" />
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/empty_conversation_banner"
|
|
||||||
layout="@layout/conversation_item_banner"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/scroll_date_header"
|
android:id="@+id/scroll_date_header"
|
||||||
style="@style/Signal.Text.Preview"
|
style="@style/Signal.Text.Preview"
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.settings.app.privacy.expire.CustomExpireTimerSelectorView
|
||||||
|
android:id="@+id/custom_expire_timer_select_dialog_selector"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:orientation="horizontal"
|
||||||
|
tools:parentTag="android.widget.LinearLayout">
|
||||||
|
|
||||||
|
<NumberPicker
|
||||||
|
android:id="@+id/custom_expire_timer_selector_value"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp" />
|
||||||
|
|
||||||
|
<NumberPicker
|
||||||
|
android:id="@+id/custom_expire_timer_selector_unit"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
|
</merge>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/dsl_preference_item_background"
|
||||||
|
android:minHeight="56dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:srcCompat="@drawable/ic_advanced_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:textAppearance="@style/Signal.Text.Body"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/summary"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/radio_widget"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
app:layout_goneMarginBottom="16dp"
|
||||||
|
app:layout_goneMarginStart="@dimen/dsl_settings_gutter"
|
||||||
|
tools:text="Message font size" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/summary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:lineSpacingExtra="4sp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||||
|
android:textColor="@color/signal_text_secondary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/radio_widget"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/title"
|
||||||
|
app:layout_goneMarginStart="@dimen/dsl_settings_gutter"
|
||||||
|
app:layout_goneMarginTop="16dp"
|
||||||
|
tools:text="Some random text to get stuff onto more than one line but not absurdly long like lorem/random"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radio_widget"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:clickable="false"
|
||||||
|
android:theme="@style/Signal.Widget.CompoundButton.RadioButton"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/dsl_settings_fragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<com.dd.CircularProgressButton
|
||||||
|
android:id="@+id/timer_select_fragment_save"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:text="@string/EditProfileNameFragment_save"
|
||||||
|
app:cornerRadius="80dp"
|
||||||
|
app:elevation="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:cpb_colorIndicator="@color/white"
|
||||||
|
app:cpb_colorProgress="?colorAccent"
|
||||||
|
app:cpb_cornerRadius="28dp"
|
||||||
|
app:cpb_selectorIdle="@drawable/progress_button_state"
|
||||||
|
app:cpb_textIdle="@string/ExpireTimerSettingsFragment__save" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/dsl_preference_item_background"
|
||||||
|
android:minHeight="56dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:srcCompat="@drawable/ic_advanced_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:textAppearance="@style/Signal.Text.Body"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/summary"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/value_client_preference_value"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
app:layout_goneMarginBottom="16dp"
|
||||||
|
app:layout_goneMarginStart="@dimen/dsl_settings_gutter"
|
||||||
|
tools:text="Message font size" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/summary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:lineSpacingExtra="4sp"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||||
|
android:textColor="@color/signal_text_secondary"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/value_client_preference_value"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/title"
|
||||||
|
app:layout_goneMarginStart="@dimen/dsl_settings_gutter"
|
||||||
|
app:layout_goneMarginTop="16dp"
|
||||||
|
tools:text="Some random text to get stuff onto more than one line but not absurdly long like lorem/random"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/value_client_preference_value"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:width="48dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||||
|
android:textColor="@color/signal_text_secondary"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Off" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -260,6 +260,14 @@
|
||||||
app:exitAnim="@anim/fragment_open_exit"
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
app:popEnterAnim="@anim/fragment_close_enter"
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
app:popExitAnim="@anim/fragment_close_exit" />
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_privacySettingsFragment_to_disappearingMessagesTimerSelectFragment"
|
||||||
|
app:destination="@id/app_settings_expire_timer"
|
||||||
|
app:enterAnim="@anim/fragment_open_enter"
|
||||||
|
app:exitAnim="@anim/fragment_open_exit"
|
||||||
|
app:popEnterAnim="@anim/fragment_close_enter"
|
||||||
|
app:popExitAnim="@anim/fragment_close_exit" />
|
||||||
|
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
@ -271,6 +279,8 @@
|
||||||
android:id="@+id/advancedPrivacySettingsFragment"
|
android:id="@+id/advancedPrivacySettingsFragment"
|
||||||
android:name="org.thoughtcrime.securesms.components.settings.app.privacy.advanced.AdvancedPrivacySettingsFragment"
|
android:name="org.thoughtcrime.securesms.components.settings.app.privacy.advanced.AdvancedPrivacySettingsFragment"
|
||||||
android:label="advanced_privacy_settings_fragment" />
|
android:label="advanced_privacy_settings_fragment" />
|
||||||
|
|
||||||
|
<include app:graph="@navigation/app_settings_expire_timer" />
|
||||||
<!-- endregion -->
|
<!-- endregion -->
|
||||||
|
|
||||||
<!-- region Data and Storage -->
|
<!-- region Data and Storage -->
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/app_settings_expire_timer"
|
||||||
|
app:startDestination="@id/expireTimerSettingsFragment">
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/expireTimerSettingsFragment"
|
||||||
|
android:name="org.thoughtcrime.securesms.components.settings.app.privacy.expire.ExpireTimerSettingsFragment"
|
||||||
|
android:label="disappearing_messages_timer_select_fragment">
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="recipient_id"
|
||||||
|
android:defaultValue="@null"
|
||||||
|
app:argType="org.thoughtcrime.securesms.recipients.RecipientId"
|
||||||
|
app:nullable="true" />
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="for_result_mode"
|
||||||
|
android:defaultValue="false"
|
||||||
|
app:argType="boolean" />
|
||||||
|
|
||||||
|
<argument
|
||||||
|
android:name="initial_value"
|
||||||
|
android:defaultValue="@null"
|
||||||
|
app:nullable="true"
|
||||||
|
app:argType="java.lang.Integer" />
|
||||||
|
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_expireTimerSettingsFragment_to_customExpireTimerSelectDialog"
|
||||||
|
app:destination="@id/customExpireTimerSelectDialog" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
android:id="@+id/customExpireTimerSelectDialog"
|
||||||
|
android:name="org.thoughtcrime.securesms.components.settings.app.privacy.expire.CustomExpireTimerSelectDialog"
|
||||||
|
tools:layout="@layout/custom_expire_timer_select_dialog" />
|
||||||
|
|
||||||
|
</navigation>
|
|
@ -360,4 +360,34 @@
|
||||||
<item>@string/MediaOverviewActivity_Storage_used</item>
|
<item>@string/MediaOverviewActivity_Storage_used</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="ExpireTimerSettingsFragment__labels">
|
||||||
|
<item>@string/ExpireTimerSettingsFragment__off</item>
|
||||||
|
<item>@string/ExpireTimerSettingsFragment__4_weeks</item>
|
||||||
|
<item>@string/ExpireTimerSettingsFragment__1_week</item>
|
||||||
|
<item>@string/ExpireTimerSettingsFragment__1_day</item>
|
||||||
|
<item>@string/ExpireTimerSettingsFragment__8_hours</item>
|
||||||
|
<item>@string/ExpireTimerSettingsFragment__1_hour</item>
|
||||||
|
<item>@string/ExpireTimerSettingsFragment__5_minutes</item>
|
||||||
|
<item>@string/ExpireTimerSettingsFragment__30_seconds</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<integer-array name="ExpireTimerSettingsFragment__values">
|
||||||
|
<item>0</item>
|
||||||
|
<item>2419200</item>
|
||||||
|
<item>604800</item>
|
||||||
|
<item>86400</item>
|
||||||
|
<item>28800</item>
|
||||||
|
<item>3600</item>
|
||||||
|
<item>300</item>
|
||||||
|
<item>30</item>
|
||||||
|
</integer-array>
|
||||||
|
|
||||||
|
<string-array name="CustomExpireTimerSelectorView__unit_labels">
|
||||||
|
<item>@string/CustomExpireTimerSelectorView__second</item>
|
||||||
|
<item>@string/CustomExpireTimerSelectorView__minute</item>
|
||||||
|
<item>@string/CustomExpireTimerSelectorView__hour</item>
|
||||||
|
<item>@string/CustomExpireTimerSelectorView__day</item>
|
||||||
|
<item>@string/CustomExpireTimerSelectorView__week</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1907,6 +1907,7 @@
|
||||||
<string name="ConversationUpdateItem_no_groups_in_common_review_requests_carefully">No groups in common. Review requests carefully.</string>
|
<string name="ConversationUpdateItem_no_groups_in_common_review_requests_carefully">No groups in common. Review requests carefully.</string>
|
||||||
<string name="ConversationUpdateItem_no_contacts_in_this_group_review_requests_carefully">No contacts in this group. Review requests carefully.</string>
|
<string name="ConversationUpdateItem_no_contacts_in_this_group_review_requests_carefully">No contacts in this group. Review requests carefully.</string>
|
||||||
<string name="ConversationUpdateItem_view">View</string>
|
<string name="ConversationUpdateItem_view">View</string>
|
||||||
|
<string name="ConversationUpdateItem_the_disappearing_message_time_will_be_set_to_s_when_you_message_them">The disappearing message time will be set to %1$s when you message them.</string>
|
||||||
|
|
||||||
<!-- audio_view -->
|
<!-- audio_view -->
|
||||||
<string name="audio_view__play_pause_accessibility_description">Play … Pause</string>
|
<string name="audio_view__play_pause_accessibility_description">Play … Pause</string>
|
||||||
|
@ -3415,15 +3416,39 @@
|
||||||
<string name="PrivacySettingsFragment__blocked">Blocked</string>
|
<string name="PrivacySettingsFragment__blocked">Blocked</string>
|
||||||
<string name="PrivacySettingsFragment__d_contacts">%1$d contacts</string>
|
<string name="PrivacySettingsFragment__d_contacts">%1$d contacts</string>
|
||||||
<string name="PrivacySettingsFragment__messaging">Messaging</string>
|
<string name="PrivacySettingsFragment__messaging">Messaging</string>
|
||||||
|
<string name="PrivacySettingsFragment__disappearing_messages">Disappearing messages</string>
|
||||||
<string name="PrivacySettingsFragment__app_security">App security</string>
|
<string name="PrivacySettingsFragment__app_security">App security</string>
|
||||||
<string name="PrivacySettingsFragment__block_screenshots_in_the_recents_list_and_inside_the_app">Block screenshots in the recents list and inside the app</string>
|
<string name="PrivacySettingsFragment__block_screenshots_in_the_recents_list_and_inside_the_app">Block screenshots in the recents list and inside the app</string>
|
||||||
<string name="PrivacySettingsFragment__signal_message_and_calls">Signal messages and calls, always relay calls, and sealed sender</string>
|
<string name="PrivacySettingsFragment__signal_message_and_calls">Signal messages and calls, always relay calls, and sealed sender</string>
|
||||||
|
<string name="PrivacySettingsFragment__default_timer_for_new_changes">Default timer for new chats</string>
|
||||||
|
<string name="PrivacySettingsFragment__set_a_default_disappearing_message_timer_for_all_new_chats_started_by_you">Set a default disappearing message timer for all new chats started by you.</string>
|
||||||
|
|
||||||
<!-- AdvancedPrivacySettingsFragment -->
|
<!-- AdvancedPrivacySettingsFragment -->
|
||||||
<string name="AdvancedPrivacySettingsFragment__sealed_sender_link" translatable="false">https://signal.org/blog/sealed-sender</string>
|
<string name="AdvancedPrivacySettingsFragment__sealed_sender_link" translatable="false">https://signal.org/blog/sealed-sender</string>
|
||||||
<string name="AdvancedPrivacySettingsFragment__show_status_icon">Show status icon</string>
|
<string name="AdvancedPrivacySettingsFragment__show_status_icon">Show status icon</string>
|
||||||
<string name="AdvancedPrivacySettingsFragment__show_an_icon">Show an icon in message details when they were delivered using sealed sender.</string>
|
<string name="AdvancedPrivacySettingsFragment__show_an_icon">Show an icon in message details when they were delivered using sealed sender.</string>
|
||||||
|
|
||||||
|
<!-- ExpireTimerSettingsFragment -->
|
||||||
|
<string name="ExpireTimerSettingsFragment__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 new chats started by you will disappear after they have been seen.</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__when_enabled_new_messages_sent_and_received_in_this_chat_will_disappear_after_they_have_been_seen">When enabled, new messages sent and received in this chat will disappear after they have been seen.</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__off">Off</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__4_weeks">4 weeks</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__1_week">1 week</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__1_day">1 day</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__8_hours">8 hours</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__1_hour">1 hour</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__5_minutes">5 minutes</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__30_seconds">30 seconds</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__custom_time">Custom time</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__set">Set</string>
|
||||||
|
<string name="ExpireTimerSettingsFragment__save">Save</string>
|
||||||
|
|
||||||
|
<string name="CustomExpireTimerSelectorView__second">Second</string>
|
||||||
|
<string name="CustomExpireTimerSelectorView__minute">Minute</string>
|
||||||
|
<string name="CustomExpireTimerSelectorView__hour">Hour</string>
|
||||||
|
<string name="CustomExpireTimerSelectorView__day">Day</string>
|
||||||
|
<string name="CustomExpireTimerSelectorView__week">Week</string>
|
||||||
|
|
||||||
<!-- HelpSettingsFragment -->
|
<!-- HelpSettingsFragment -->
|
||||||
<string name="HelpSettingsFragment__support_center">Support center</string>
|
<string name="HelpSettingsFragment__support_center">Support center</string>
|
||||||
<string name="HelpSettingsFragment__contact_us">Contact us</string>
|
<string name="HelpSettingsFragment__contact_us">Contact us</string>
|
||||||
|
|
|
@ -127,6 +127,10 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||||
diff.add("Payments");
|
diff.add("Payments");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.getUniversalExpireTimer() != that.getUniversalExpireTimer()) {
|
||||||
|
diff.add("UniversalExpireTimer");
|
||||||
|
}
|
||||||
|
|
||||||
if (!Objects.equals(this.isPrimarySendsSms(), that.isPrimarySendsSms())) {
|
if (!Objects.equals(this.isPrimarySendsSms(), that.isPrimarySendsSms())) {
|
||||||
diff.add("PrimarySendsSms");
|
diff.add("PrimarySendsSms");
|
||||||
}
|
}
|
||||||
|
@ -209,6 +213,10 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||||
return payments;
|
return payments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getUniversalExpireTimer() {
|
||||||
|
return proto.getUniversalExpireTimer();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isPrimarySendsSms() {
|
public boolean isPrimarySendsSms() {
|
||||||
return proto.getPrimarySendsSms();
|
return proto.getPrimarySendsSms();
|
||||||
}
|
}
|
||||||
|
@ -467,6 +475,11 @@ public final class SignalAccountRecord implements SignalRecord {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder setUniversalExpireTimer(int timer) {
|
||||||
|
builder.setUniversalExpireTimer(timer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder setPrimarySendsSms(boolean primarySendsSms) {
|
public Builder setPrimarySendsSms(boolean primarySendsSms) {
|
||||||
builder.setPrimarySendsSms(primarySendsSms);
|
builder.setPrimarySendsSms(primarySendsSms);
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -144,5 +144,6 @@ message AccountRecord {
|
||||||
repeated PinnedConversation pinnedConversations = 14;
|
repeated PinnedConversation pinnedConversations = 14;
|
||||||
bool preferContactAvatars = 15;
|
bool preferContactAvatars = 15;
|
||||||
Payments payments = 16;
|
Payments payments = 16;
|
||||||
|
uint32 universalExpireTimer = 17;
|
||||||
bool primarySendsSms = 18;
|
bool primarySendsSms = 18;
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue