Add universal disappearing messages.

fork-5.53.8
Cody Henthorne 2021-05-18 15:19:33 -04:00 zatwierdzone przez Greyson Parrelli
rodzic 8c6a88374b
commit defd5e8047
70 zmienionych plików z 1513 dodań i 251 usunięć

Wyświetl plik

@ -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"

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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)
}
}
} }

Wyświetl plik

@ -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
) )

Wyświetl plik

@ -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
) )
} }

Wyświetl plik

@ -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)
}
}
}

Wyświetl plik

@ -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]
}
}
}

Wyświetl plik

@ -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
)
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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
)
}

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)));

Wyświetl plik

@ -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;

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);
} }
}); });

Wyświetl plik

@ -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);
} }
} }

Wyświetl plik

@ -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());
} }

Wyświetl plik

@ -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);

Wyświetl plik

@ -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();

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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,

Wyświetl plik

@ -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()) {

Wyświetl plik

@ -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;
}
}
} }

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);
} }

Wyświetl plik

@ -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;

Wyświetl plik

@ -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);
} }

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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;

Wyświetl plik

@ -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()));

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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);
} }

Wyświetl plik

@ -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());

Wyświetl plik

@ -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, "");

Wyświetl plik

@ -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;
} }

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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() ||

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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()));

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);
} }

Wyświetl plik

@ -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;

Wyświetl plik

@ -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)) {

Wyświetl plik

@ -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;
} }

Wyświetl plik

@ -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);
} }

Wyświetl plik

@ -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()));

Wyświetl plik

@ -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];

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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())
}
}
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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"

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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 -->

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;
} }