kopia lustrzana https://github.com/ryukoposting/Signal-Android
Implement several pieces of UI polish for badges.
rodzic
186bd9db48
commit
755ec672c0
|
@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode
|
import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||||
import org.thoughtcrime.securesms.util.ScreenDensity
|
import org.thoughtcrime.securesms.util.ScreenDensity
|
||||||
import org.whispersystems.libsignal.util.Pair
|
import org.whispersystems.libsignal.util.Pair
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||||
|
@ -59,8 +60,7 @@ object Badges {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBestBadgeImageUriForDevice(serviceBadge: SignalServiceProfile.Badge): Pair<Uri, String> {
|
private fun getBestBadgeImageUriForDevice(serviceBadge: SignalServiceProfile.Badge): Pair<Uri, String> {
|
||||||
val bestDensity = ScreenDensity.getBestDensityBucketForDevice()
|
return when (ScreenDensity.getBestDensityBucketForDevice()) {
|
||||||
return when (bestDensity) {
|
|
||||||
"ldpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[0]), "ldpi")
|
"ldpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[0]), "ldpi")
|
||||||
"mdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[1]), "mdpi")
|
"mdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[1]), "mdpi")
|
||||||
"hdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[2]), "hdpi")
|
"hdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[2]), "hdpi")
|
||||||
|
@ -74,6 +74,34 @@ object Badges {
|
||||||
return Timestamp(bigDecimal.toLong() * 1000).time
|
return Timestamp(bigDecimal.toLong() * 1000).time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun fromDatabaseBadge(badge: BadgeList.Badge): Badge {
|
||||||
|
return Badge(
|
||||||
|
badge.id,
|
||||||
|
fromCode(badge.category),
|
||||||
|
badge.name,
|
||||||
|
badge.description,
|
||||||
|
Uri.parse(badge.imageUrl),
|
||||||
|
badge.imageDensity,
|
||||||
|
badge.expiration,
|
||||||
|
badge.visible
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun toDatabaseBadge(badge: Badge): BadgeList.Badge {
|
||||||
|
return BadgeList.Badge.newBuilder()
|
||||||
|
.setId(badge.id)
|
||||||
|
.setCategory(badge.category.code)
|
||||||
|
.setDescription(badge.description)
|
||||||
|
.setExpiration(badge.expirationTimestamp)
|
||||||
|
.setVisible(badge.visible)
|
||||||
|
.setName(badge.name)
|
||||||
|
.setImageUrl(badge.imageUrl.toString())
|
||||||
|
.setImageDensity(badge.imageDensity)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun fromServiceBadge(serviceBadge: SignalServiceProfile.Badge): Badge {
|
fun fromServiceBadge(serviceBadge: SignalServiceProfile.Badge): Badge {
|
||||||
val uriAndDensity: Pair<Uri, String> = getBestBadgeImageUriForDevice(serviceBadge)
|
val uriAndDensity: Pair<Uri, String> = getBestBadgeImageUriForDevice(serviceBadge)
|
||||||
|
|
|
@ -37,6 +37,7 @@ data class Badge(
|
||||||
) : Parcelable, Key {
|
) : Parcelable, Key {
|
||||||
|
|
||||||
fun isExpired(): Boolean = expirationTimestamp < System.currentTimeMillis()
|
fun isExpired(): Boolean = expirationTimestamp < System.currentTimeMillis()
|
||||||
|
fun isBoost(): Boolean = id == BOOST_BADGE_ID
|
||||||
|
|
||||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||||
messageDigest.update(id.toByteArray(Key.CHARSET))
|
messageDigest.update(id.toByteArray(Key.CHARSET))
|
||||||
|
@ -159,6 +160,8 @@ data class Badge(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val BOOST_BADGE_ID = "BOOST"
|
||||||
|
|
||||||
private val SELECTION_CHANGED = Any()
|
private val SELECTION_CHANGED = Any()
|
||||||
|
|
||||||
fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) {
|
fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) {
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
package org.thoughtcrime.securesms.badges.self.expired
|
package org.thoughtcrime.securesms.badges.self.expired
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.signal.core.util.DimensionUnit
|
import org.signal.core.util.DimensionUnit
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.badges.models.ExpiredBadge
|
import org.thoughtcrime.securesms.badges.models.ExpiredBadge
|
||||||
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.DSLSettingsBottomSheetFragment
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||||
import org.thoughtcrime.securesms.components.settings.configure
|
import org.thoughtcrime.securesms.components.settings.configure
|
||||||
|
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bottom sheet displaying a fading badge with a notice and action for becoming a subscriber again.
|
* Bottom sheet displaying a fading badge with a notice and action for becoming a subscriber again.
|
||||||
|
@ -28,13 +31,23 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||||
return configure {
|
return configure {
|
||||||
customPref(ExpiredBadge.Model(badge))
|
customPref(ExpiredBadge.Model(badge))
|
||||||
|
|
||||||
sectionHeaderPref(R.string.ExpiredBadgeBottomSheetDialogFragment__your_badge_has_expired)
|
sectionHeaderPref(
|
||||||
|
if (badge.isBoost()) {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__your_badge_has_expired
|
||||||
|
} else {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__your_subscription_was_cancelled
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
space(DimensionUnit.DP.toPixels(4f).toInt())
|
space(DimensionUnit.DP.toPixels(4f).toInt())
|
||||||
|
|
||||||
noPadTextPref(
|
noPadTextPref(
|
||||||
DSLSettingsText.from(
|
DSLSettingsText.from(
|
||||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_s_badge_has_expired, badge.name),
|
if (badge.isBoost()) {
|
||||||
|
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_s_badge_has_expired, badge.name)
|
||||||
|
} else {
|
||||||
|
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__because)
|
||||||
|
},
|
||||||
DSLSettingsText.CenterModifier
|
DSLSettingsText.CenterModifier
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -43,7 +56,11 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||||
|
|
||||||
noPadTextPref(
|
noPadTextPref(
|
||||||
DSLSettingsText.from(
|
DSLSettingsText.from(
|
||||||
R.string.ExpiredBadgeBottomSheetDialogFragment__to_continue_supporting,
|
if (badge.isBoost()) {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__to_continue_supporting
|
||||||
|
} else {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__to_continue_supporting_signal
|
||||||
|
},
|
||||||
DSLSettingsText.CenterModifier
|
DSLSettingsText.CenterModifier
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -51,7 +68,13 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||||
space(DimensionUnit.DP.toPixels(92f).toInt())
|
space(DimensionUnit.DP.toPixels(92f).toInt())
|
||||||
|
|
||||||
primaryButton(
|
primaryButton(
|
||||||
text = DSLSettingsText.from(R.string.ExpiredBadgeBottomSheetDialogFragment__become_a_subscriber),
|
text = DSLSettingsText.from(
|
||||||
|
if (badge.isBoost()) {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__become_a_subscriber
|
||||||
|
} else {
|
||||||
|
R.string.ExpiredBadgeBottomSheetDialogFragment__renew_subscription
|
||||||
|
}
|
||||||
|
),
|
||||||
onClick = {
|
onClick = {
|
||||||
dismiss()
|
dismiss()
|
||||||
findNavController().navigate(R.id.action_directly_to_subscribe)
|
findNavController().navigate(R.id.action_directly_to_subscribe)
|
||||||
|
@ -66,4 +89,15 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun show(badge: Badge, fragmentManager: FragmentManager) {
|
||||||
|
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge).build()
|
||||||
|
val fragment = ExpiredBadgeBottomSheetDialogFragment()
|
||||||
|
fragment.arguments = args.toBundle()
|
||||||
|
|
||||||
|
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
@ -15,10 +16,13 @@ import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
import org.thoughtcrime.securesms.badges.models.LargeBadge
|
import org.thoughtcrime.securesms.badges.models.LargeBadge
|
||||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||||
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.util.BottomSheetUtil
|
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
|
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||||
import org.thoughtcrime.securesms.util.visible
|
import org.thoughtcrime.securesms.util.visible
|
||||||
|
|
||||||
class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||||
|
@ -37,11 +41,25 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
||||||
val pager: ViewPager2 = view.findViewById(R.id.pager)
|
val pager: ViewPager2 = view.findViewById(R.id.pager)
|
||||||
val tabs: TabLayout = view.findViewById(R.id.tab_layout)
|
val tabs: TabLayout = view.findViewById(R.id.tab_layout)
|
||||||
val action: MaterialButton = view.findViewById(R.id.action)
|
val action: MaterialButton = view.findViewById(R.id.action)
|
||||||
|
val noSupport: View = view.findViewById(R.id.no_support)
|
||||||
|
|
||||||
if (getRecipientId() == Recipient.self().id) {
|
if (getRecipientId() == Recipient.self().id) {
|
||||||
action.visible = false
|
action.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PlayServicesUtil.getPlayServicesStatus(requireContext()) != PlayServicesUtil.PlayServicesStatus.SUCCESS) {
|
||||||
|
noSupport.visible = true
|
||||||
|
action.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_open_20)
|
||||||
|
action.setText(R.string.preferences__donate_to_signal)
|
||||||
|
action.setOnClickListener {
|
||||||
|
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
action.setOnClickListener {
|
||||||
|
startActivity(AppSettingsActivity.subscriptions(requireContext()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val adapter = MappingAdapter()
|
val adapter = MappingAdapter()
|
||||||
|
|
||||||
LargeBadge.register(adapter)
|
LargeBadge.register(adapter)
|
||||||
|
|
|
@ -70,4 +70,10 @@ sealed class DSLSettingsText {
|
||||||
return SpanUtil.textAppearance(context, textAppearance, charSequence)
|
return SpanUtil.textAppearance(context, textAppearance, charSequence)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object BoldModifier : Modifier {
|
||||||
|
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
||||||
|
return SpanUtil.bold(charSequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||||
|
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||||
|
|
||||||
class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__menu_settings) {
|
class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__menu_settings) {
|
||||||
|
|
||||||
|
@ -143,7 +144,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (FeatureFlags.donorBadges()) {
|
if (FeatureFlags.donorBadges() && PlayServicesUtil.getPlayServicesStatus(requireContext()) == PlayServicesUtil.PlayServicesStatus.SUCCESS) {
|
||||||
customPref(
|
customPref(
|
||||||
SubscriptionPreference(
|
SubscriptionPreference(
|
||||||
title = DSLSettingsText.from(R.string.preferences__subscription),
|
title = DSLSettingsText.from(R.string.preferences__subscription),
|
||||||
|
|
|
@ -106,15 +106,10 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||||
return Completable.create {
|
return Completable.create {
|
||||||
stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), paymentIntent).blockingSubscribe()
|
stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), paymentIntent).blockingSubscribe()
|
||||||
|
|
||||||
val jobIds = BoostReceiptRequestResponseJob.enqueueChain(paymentIntent)
|
val jobId = BoostReceiptRequestResponseJob.enqueueChain(paymentIntent)
|
||||||
val countDownLatch = CountDownLatch(2)
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
|
||||||
ApplicationDependencies.getJobManager().addListener(jobIds.first()) { _, jobState ->
|
ApplicationDependencies.getJobManager().addListener(jobId) { _, jobState ->
|
||||||
if (jobState.isComplete) {
|
|
||||||
countDownLatch.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ApplicationDependencies.getJobManager().addListener(jobIds.second()) { _, jobState ->
|
|
||||||
if (jobState.isComplete) {
|
if (jobState.isComplete) {
|
||||||
countDownLatch.countDown()
|
countDownLatch.countDown()
|
||||||
}
|
}
|
||||||
|
@ -146,15 +141,10 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||||
SignalStore.donationsValues().clearLevelOperation(levelUpdateOperation)
|
SignalStore.donationsValues().clearLevelOperation(levelUpdateOperation)
|
||||||
it.onComplete()
|
it.onComplete()
|
||||||
}.andThen {
|
}.andThen {
|
||||||
val jobIds = SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation()
|
val jobId = SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation()
|
||||||
val countDownLatch = CountDownLatch(2)
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
|
||||||
ApplicationDependencies.getJobManager().addListener(jobIds.first()) { _, jobState ->
|
ApplicationDependencies.getJobManager().addListener(jobId) { _, jobState ->
|
||||||
if (jobState.isComplete) {
|
|
||||||
countDownLatch.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ApplicationDependencies.getJobManager().addListener(jobIds.second()) { _, jobState ->
|
|
||||||
if (jobState.isComplete) {
|
if (jobState.isComplete) {
|
||||||
countDownLatch.countDown()
|
countDownLatch.countDown()
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,9 @@ data class Boost(
|
||||||
|
|
||||||
if (model.isCustomAmountFocused && !custom.hasFocus()) {
|
if (model.isCustomAmountFocused && !custom.hasFocus()) {
|
||||||
ViewUtil.focusAndShowKeyboard(custom)
|
ViewUtil.focusAndShowKeyboard(custom)
|
||||||
|
} else if (!model.isCustomAmountFocused && custom.hasFocus()) {
|
||||||
|
ViewUtil.hideKeyboard(context, custom)
|
||||||
|
custom.clearFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,7 +146,7 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||||
text = DSLSettingsText.from(R.string.SubscribeFragment__more_payment_options),
|
text = DSLSettingsText.from(R.string.SubscribeFragment__more_payment_options),
|
||||||
icon = DSLSettingsIcon.from(R.drawable.ic_open_20, R.color.signal_accent_primary),
|
icon = DSLSettingsIcon.from(R.drawable.ic_open_20, R.color.signal_accent_primary),
|
||||||
onClick = {
|
onClick = {
|
||||||
// TODO
|
// TODO [alex] -- Where's this go?
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,12 +28,12 @@ class SetCurrencyFragment : DSLSettingsBottomSheetFragment() {
|
||||||
private fun getConfiguration(state: SetCurrencyState): DSLConfiguration {
|
private fun getConfiguration(state: SetCurrencyState): DSLConfiguration {
|
||||||
return configure {
|
return configure {
|
||||||
state.currencies.forEach { currency ->
|
state.currencies.forEach { currency ->
|
||||||
radioPref(
|
clickPref(
|
||||||
title = DSLSettingsText.from(currency.getDisplayName(Locale.getDefault())),
|
title = DSLSettingsText.from(currency.getDisplayName(Locale.getDefault())),
|
||||||
summary = DSLSettingsText.from(currency.currencyCode),
|
summary = DSLSettingsText.from(currency.currencyCode),
|
||||||
isChecked = currency.currencyCode == state.selectedCurrencyCode,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setSelectedCurrency(currency.currencyCode)
|
viewModel.setSelectedCurrency(currency.currencyCode)
|
||||||
|
dismissAllowingStateLoss()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,10 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import org.signal.donations.StripeApi
|
import org.signal.donations.StripeApi
|
||||||
import org.thoughtcrime.securesms.BuildConfig
|
import org.thoughtcrime.securesms.BuildConfig
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
|
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||||
import org.thoughtcrime.securesms.util.livedata.Store
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -43,7 +46,21 @@ class SetCurrencyViewModel(private val isBoost: Boolean) : ViewModel() {
|
||||||
if (isBoost) {
|
if (isBoost) {
|
||||||
SignalStore.donationsValues().setBoostCurrency(Currency.getInstance(selectedCurrencyCode))
|
SignalStore.donationsValues().setBoostCurrency(Currency.getInstance(selectedCurrencyCode))
|
||||||
} else {
|
} else {
|
||||||
SignalStore.donationsValues().setSubscriptionCurrency(Currency.getInstance(selectedCurrencyCode))
|
val currency = Currency.getInstance(selectedCurrencyCode)
|
||||||
|
val subscriber = SignalStore.donationsValues().getSubscriber(currency)
|
||||||
|
|
||||||
|
if (subscriber != null) {
|
||||||
|
SignalStore.donationsValues().setSubscriber(subscriber)
|
||||||
|
} else {
|
||||||
|
SignalStore.donationsValues().setSubscriber(
|
||||||
|
Subscriber(
|
||||||
|
subscriberId = SubscriberId.generate(),
|
||||||
|
currencyCode = currency.currencyCode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
StorageSyncHelper.scheduleSyncForDataChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.navigation.NavOptions
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.signal.core.util.DimensionUnit
|
import org.signal.core.util.DimensionUnit
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||||
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
|
||||||
|
@ -59,6 +60,7 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
||||||
|
|
||||||
ActiveSubscriptionPreference.register(adapter)
|
ActiveSubscriptionPreference.register(adapter)
|
||||||
IndeterminateLoadingCircle.register(adapter)
|
IndeterminateLoadingCircle.register(adapter)
|
||||||
|
BadgePreview.register(adapter)
|
||||||
|
|
||||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||||
|
@ -75,6 +77,14 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
||||||
|
|
||||||
private fun getConfiguration(state: ManageDonationsState): DSLConfiguration {
|
private fun getConfiguration(state: ManageDonationsState): DSLConfiguration {
|
||||||
return configure {
|
return configure {
|
||||||
|
customPref(
|
||||||
|
BadgePreview.Model(
|
||||||
|
badge = state.featuredBadge
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
space(DimensionUnit.DP.toPixels(8f).toInt())
|
||||||
|
|
||||||
sectionHeaderPref(
|
sectionHeaderPref(
|
||||||
title = DSLSettingsText.from(
|
title = DSLSettingsText.from(
|
||||||
R.string.SubscribeFragment__signal_is_powered_by_people_like_you,
|
R.string.SubscribeFragment__signal_is_powered_by_people_like_you,
|
||||||
|
@ -82,10 +92,12 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
space(DimensionUnit.DP.toPixels(32f).toInt())
|
||||||
|
|
||||||
noPadTextPref(
|
noPadTextPref(
|
||||||
title = DSLSettingsText.from(
|
title = DSLSettingsText.from(
|
||||||
R.string.ManageDonationsFragment__my_support,
|
R.string.ManageDonationsFragment__my_support,
|
||||||
DSLSettingsText.Title2BoldModifier
|
DSLSettingsText.Body1BoldModifier, DSLSettingsText.BoldModifier
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -94,7 +106,7 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
||||||
if (activeSubscription.isActive) {
|
if (activeSubscription.isActive) {
|
||||||
val subscription: Subscription? = state.availableSubscriptions.firstOrNull { activeSubscription.activeSubscription.level == it.level }
|
val subscription: Subscription? = state.availableSubscriptions.firstOrNull { activeSubscription.activeSubscription.level == it.level }
|
||||||
if (subscription != null) {
|
if (subscription != null) {
|
||||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
space(DimensionUnit.DP.toPixels(12f).toInt())
|
||||||
|
|
||||||
customPref(
|
customPref(
|
||||||
ActiveSubscriptionPreference.Model(
|
ActiveSubscriptionPreference.Model(
|
||||||
|
|
|
@ -124,6 +124,8 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
space(DimensionUnit.DP.toPixels(4f).toInt())
|
||||||
|
|
||||||
state.subscriptions.forEach {
|
state.subscriptions.forEach {
|
||||||
val isActive = state.activeSubscription?.activeSubscription?.level == it.level
|
val isActive = state.activeSubscription?.activeSubscription?.level == it.level
|
||||||
customPref(
|
customPref(
|
||||||
|
|
|
@ -49,13 +49,19 @@ object AvatarPreference {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(model: Model) {
|
override fun bind(model: Model) {
|
||||||
badge.setBadgeFromRecipient(model.recipient)
|
if (model.recipient.isSelf) {
|
||||||
badge.setOnClickListener {
|
badge.setBadge(null)
|
||||||
val badge = model.recipient.badges.firstOrNull()
|
badge.setOnClickListener(null)
|
||||||
if (badge != null) {
|
} else {
|
||||||
model.onBadgeClick(badge)
|
badge.setBadgeFromRecipient(model.recipient)
|
||||||
|
badge.setOnClickListener {
|
||||||
|
val badge = model.recipient.badges.firstOrNull()
|
||||||
|
if (badge != null) {
|
||||||
|
model.onBadgeClick(badge)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
avatar.setAvatar(model.recipient)
|
avatar.setAvatar(model.recipient)
|
||||||
avatar.disableQuickContact()
|
avatar.disableQuickContact()
|
||||||
avatar.setOnClickListener { model.onAvatarClick(avatar) }
|
avatar.setOnClickListener { model.onAvatarClick(avatar) }
|
||||||
|
|
|
@ -121,7 +121,11 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
|
||||||
|
|
||||||
this.checkBox.setVisibility(checkboxVisible ? View.VISIBLE : View.GONE);
|
this.checkBox.setVisibility(checkboxVisible ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
badge.setBadgeFromRecipient(recipientSnapshot);
|
if (recipientSnapshot == null || recipientSnapshot.isSelf()) {
|
||||||
|
badge.setBadge(null);
|
||||||
|
} else {
|
||||||
|
badge.setBadgeFromRecipient(recipientSnapshot);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setChecked(boolean selected, boolean animate) {
|
public void setChecked(boolean selected, boolean animate) {
|
||||||
|
|
|
@ -57,7 +57,11 @@ public class ConversationBannerView extends ConstraintLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBadge(@Nullable Recipient recipient) {
|
public void setBadge(@Nullable Recipient recipient) {
|
||||||
contactBadge.setBadgeFromRecipient(recipient);
|
if (recipient == null || recipient.isSelf()) {
|
||||||
|
contactBadge.setBadge(null);
|
||||||
|
} else {
|
||||||
|
contactBadge.setBadgeFromRecipient(recipient);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAvatar(@NonNull GlideRequests requests, @Nullable Recipient recipient) {
|
public void setAvatar(@NonNull GlideRequests requests, @Nullable Recipient recipient) {
|
||||||
|
|
|
@ -106,7 +106,11 @@ public class ConversationTitleView extends RelativeLayout {
|
||||||
this.avatar.setAvatar(glideRequests, recipient, false);
|
this.avatar.setAvatar(glideRequests, recipient, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
badge.setBadgeFromRecipient(recipient);
|
if (recipient == null || recipient.isSelf()) {
|
||||||
|
badge.setBadgeFromRecipient(null);
|
||||||
|
} else {
|
||||||
|
badge.setBadgeFromRecipient(recipient);
|
||||||
|
}
|
||||||
|
|
||||||
updateVerifiedSubtitleVisibility();
|
updateVerifiedSubtitleVisibility();
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,8 @@ import org.thoughtcrime.securesms.MainNavigator;
|
||||||
import org.thoughtcrime.securesms.MuteDialog;
|
import org.thoughtcrime.securesms.MuteDialog;
|
||||||
import org.thoughtcrime.securesms.NewConversationActivity;
|
import org.thoughtcrime.securesms.NewConversationActivity;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||||
|
import org.thoughtcrime.securesms.badges.self.expired.ExpiredBadgeBottomSheetDialogFragment;
|
||||||
import org.thoughtcrime.securesms.components.RatingManager;
|
import org.thoughtcrime.securesms.components.RatingManager;
|
||||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||||
|
@ -322,6 +324,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
Log.i(TAG, "Recaptcha required.");
|
Log.i(TAG, "Recaptcha required.");
|
||||||
RecaptchaProofBottomSheetFragment.show(getChildFragmentManager());
|
RecaptchaProofBottomSheetFragment.show(getChildFragmentManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Badge expiredBadge = SignalStore.donationsValues().getExpiredBadge();
|
||||||
|
if (expiredBadge != null) {
|
||||||
|
SignalStore.donationsValues().setExpiredBadge(null);
|
||||||
|
ExpiredBadgeBottomSheetDialogFragment.show(expiredBadge, getParentFragmentManager());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.signal.zkgroup.InvalidInputException;
|
||||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
|
import org.thoughtcrime.securesms.badges.Badges;
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||||
|
@ -1370,16 +1371,7 @@ public class RecipientDatabase extends Database {
|
||||||
List<BadgeList.Badge> protoBadges = badgeList.getBadgesList();
|
List<BadgeList.Badge> protoBadges = badgeList.getBadgesList();
|
||||||
badges = new ArrayList<>(protoBadges.size());
|
badges = new ArrayList<>(protoBadges.size());
|
||||||
for (BadgeList.Badge protoBadge : protoBadges) {
|
for (BadgeList.Badge protoBadge : protoBadges) {
|
||||||
badges.add(new Badge(
|
badges.add(Badges.fromDatabaseBadge(protoBadge));
|
||||||
protoBadge.getId(),
|
|
||||||
Badge.Category.Companion.fromCode(protoBadge.getCategory()),
|
|
||||||
protoBadge.getName(),
|
|
||||||
protoBadge.getDescription(),
|
|
||||||
Uri.parse(protoBadge.getImageUrl()),
|
|
||||||
protoBadge.getImageDensity(),
|
|
||||||
protoBadge.getExpiration(),
|
|
||||||
protoBadge.getVisible()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
badges = Collections.emptyList();
|
badges = Collections.emptyList();
|
||||||
|
@ -1712,15 +1704,7 @@ public class RecipientDatabase extends Database {
|
||||||
BadgeList.Builder badgeListBuilder = BadgeList.newBuilder();
|
BadgeList.Builder badgeListBuilder = BadgeList.newBuilder();
|
||||||
|
|
||||||
for (final Badge badge : badges) {
|
for (final Badge badge : badges) {
|
||||||
badgeListBuilder.addBadges(BadgeList.Badge.newBuilder()
|
badgeListBuilder.addBadges(Badges.toDatabaseBadge(badge));
|
||||||
.setId(badge.getId())
|
|
||||||
.setCategory(badge.getCategory().getCode())
|
|
||||||
.setDescription(badge.getDescription())
|
|
||||||
.setExpiration(badge.getExpirationTimestamp())
|
|
||||||
.setVisible(badge.getVisible())
|
|
||||||
.setName(badge.getName())
|
|
||||||
.setImageUrl(badge.getImageUrl().toString())
|
|
||||||
.setImageDensity(badge.getImageDensity()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentValues values = new ContentValues(1);
|
ContentValues values = new ContentValues(1);
|
||||||
|
|
|
@ -59,16 +59,18 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Pair<String, String> enqueueChain(StripeApi.PaymentIntent paymentIntent) {
|
public static String enqueueChain(StripeApi.PaymentIntent paymentIntent) {
|
||||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntent);
|
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntent);
|
||||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost();
|
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost();
|
||||||
|
RefreshOwnProfileJob refreshOwnProfileJob = new RefreshOwnProfileJob();
|
||||||
|
|
||||||
ApplicationDependencies.getJobManager()
|
ApplicationDependencies.getJobManager()
|
||||||
.startChain(requestReceiptJob)
|
.startChain(requestReceiptJob)
|
||||||
.then(redeemReceiptJob)
|
.then(redeemReceiptJob)
|
||||||
|
.then(refreshOwnProfileJob)
|
||||||
.enqueue();
|
.enqueue();
|
||||||
|
|
||||||
return new Pair<>(requestReceiptJob.getId(), redeemReceiptJob.getId());
|
return refreshOwnProfileJob.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
private BoostReceiptRequestResponseJob(@NonNull Parameters parameters,
|
private BoostReceiptRequestResponseJob(@NonNull Parameters parameters,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.thoughtcrime.securesms.jobs;
|
package org.thoughtcrime.securesms.jobs;
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
@ -9,7 +8,6 @@ import androidx.annotation.Nullable;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.thoughtcrime.securesms.BuildConfig;
|
|
||||||
import org.thoughtcrime.securesms.badges.Badges;
|
import org.thoughtcrime.securesms.badges.Badges;
|
||||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||||
|
@ -23,10 +21,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||||
import org.thoughtcrime.securesms.util.ScreenDensity;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
import org.whispersystems.libsignal.util.Pair;
|
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
|
||||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||||
|
@ -34,9 +30,10 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.util.Comparator;
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
|
@ -175,11 +172,60 @@ public class RefreshOwnProfileJob extends BaseJob {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Set<String> localDonorBadgeIds = Recipient.self()
|
||||||
|
.getBadges()
|
||||||
|
.stream()
|
||||||
|
.filter(badge -> badge.getCategory() == Badge.Category.Donor)
|
||||||
|
.map(Badge::getId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Set<String> remoteDonorBadgeIds = badges.stream()
|
||||||
|
.filter(badge -> Objects.equals(badge.getCategory(), Badge.Category.Donor.getCode()))
|
||||||
|
.map(SignalServiceProfile.Badge::getId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
boolean remoteHasSubscriptionBadges = remoteDonorBadgeIds.stream().anyMatch(RefreshOwnProfileJob::isSubscription);
|
||||||
|
boolean localHasSubscriptionBadges = localDonorBadgeIds.stream().anyMatch(RefreshOwnProfileJob::isSubscription);
|
||||||
|
boolean remoteHasBoostBadges = remoteDonorBadgeIds.stream().anyMatch(RefreshOwnProfileJob::isBoost);
|
||||||
|
boolean localHasBoostBadges = localDonorBadgeIds.stream().anyMatch(RefreshOwnProfileJob::isBoost);
|
||||||
|
|
||||||
|
if (!remoteHasSubscriptionBadges && localHasSubscriptionBadges) {
|
||||||
|
Badge mostRecentExpiration = Recipient.self()
|
||||||
|
.getBadges()
|
||||||
|
.stream()
|
||||||
|
.filter(badge -> badge.getCategory() == Badge.Category.Donor)
|
||||||
|
.filter(badge -> isSubscription(badge.getId()))
|
||||||
|
.max(Comparator.comparingLong(Badge::getExpirationTimestamp))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
Log.d(TAG, "Marking subscription badge as expired, should notifiy next time the conversation list is open.");
|
||||||
|
SignalStore.donationsValues().setExpiredBadge(mostRecentExpiration);
|
||||||
|
} else if (!remoteHasBoostBadges && localHasBoostBadges) {
|
||||||
|
Badge mostRecentExpiration = Recipient.self()
|
||||||
|
.getBadges()
|
||||||
|
.stream()
|
||||||
|
.filter(badge -> badge.getCategory() == Badge.Category.Donor)
|
||||||
|
.filter(badge -> isBoost(badge.getId()))
|
||||||
|
.max(Comparator.comparingLong(Badge::getExpirationTimestamp))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
Log.d(TAG, "Marking boost badge as expired, should notifiy next time the conversation list is open.");
|
||||||
|
SignalStore.donationsValues().setExpiredBadge(mostRecentExpiration);
|
||||||
|
}
|
||||||
|
|
||||||
DatabaseFactory.getRecipientDatabase(context)
|
DatabaseFactory.getRecipientDatabase(context)
|
||||||
.setBadges(Recipient.self().getId(),
|
.setBadges(Recipient.self().getId(),
|
||||||
badges.stream().map(Badges::fromServiceBadge).collect(Collectors.toList()));
|
badges.stream().map(Badges::fromServiceBadge).collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isSubscription(String badgeId) {
|
||||||
|
return !Objects.equals(badgeId, Badge.BOOST_BADGE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBoost(String badgeId) {
|
||||||
|
return Objects.equals(badgeId, Badge.BOOST_BADGE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
public static final class Factory implements Job.Factory<RefreshOwnProfileJob> {
|
public static final class Factory implements Job.Factory<RefreshOwnProfileJob> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -64,17 +64,19 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Pair<String, String> enqueueSubscriptionContinuation() {
|
public static String enqueueSubscriptionContinuation() {
|
||||||
Subscriber subscriber = SignalStore.donationsValues().requireSubscriber();
|
Subscriber subscriber = SignalStore.donationsValues().requireSubscriber();
|
||||||
SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId());
|
SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId());
|
||||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription();
|
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription();
|
||||||
|
RefreshOwnProfileJob refreshOwnProfileJob = new RefreshOwnProfileJob();
|
||||||
|
|
||||||
ApplicationDependencies.getJobManager()
|
ApplicationDependencies.getJobManager()
|
||||||
.startChain(requestReceiptJob)
|
.startChain(requestReceiptJob)
|
||||||
.then(redeemReceiptJob)
|
.then(redeemReceiptJob)
|
||||||
|
.then(refreshOwnProfileJob)
|
||||||
.enqueue();
|
.enqueue();
|
||||||
|
|
||||||
return new Pair<>(requestReceiptJob.getId(), redeemReceiptJob.getId());
|
return refreshOwnProfileJob.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
private SubscriptionReceiptRequestResponseJob(@NonNull Parameters parameters,
|
private SubscriptionReceiptRequestResponseJob(@NonNull Parameters parameters,
|
||||||
|
|
|
@ -4,6 +4,9 @@ import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||||
import io.reactivex.rxjava3.subjects.Subject
|
import io.reactivex.rxjava3.subjects.Subject
|
||||||
import org.signal.donations.StripeApi
|
import org.signal.donations.StripeApi
|
||||||
|
import org.thoughtcrime.securesms.badges.Badges
|
||||||
|
import org.thoughtcrime.securesms.badges.models.Badge
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
|
||||||
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
|
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
|
||||||
|
@ -25,11 +28,15 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
||||||
private const val KEY_LEVEL = "donation.level"
|
private const val KEY_LEVEL = "donation.level"
|
||||||
private const val KEY_LAST_KEEP_ALIVE_LAUNCH = "donation.last.successful.ping"
|
private const val KEY_LAST_KEEP_ALIVE_LAUNCH = "donation.last.successful.ping"
|
||||||
private const val KEY_LAST_END_OF_PERIOD = "donation.last.end.of.period"
|
private const val KEY_LAST_END_OF_PERIOD = "donation.last.end.of.period"
|
||||||
|
private const val EXPIRED_BADGE = "donation.expired.badge"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFirstEverAppLaunch() = Unit
|
override fun onFirstEverAppLaunch() = Unit
|
||||||
|
|
||||||
override fun getKeysToIncludeInBackup(): MutableList<String> = mutableListOf(KEY_SUBSCRIPTION_CURRENCY_CODE, KEY_LAST_KEEP_ALIVE_LAUNCH)
|
override fun getKeysToIncludeInBackup(): MutableList<String> = mutableListOf(
|
||||||
|
KEY_CURRENCY_CODE_BOOST,
|
||||||
|
KEY_LAST_KEEP_ALIVE_LAUNCH
|
||||||
|
)
|
||||||
|
|
||||||
private val subscriptionCurrencyPublisher: Subject<Currency> by lazy { BehaviorSubject.createDefault(getSubscriptionCurrency()) }
|
private val subscriptionCurrencyPublisher: Subject<Currency> by lazy { BehaviorSubject.createDefault(getSubscriptionCurrency()) }
|
||||||
val observableSubscriptionCurrency: Observable<Currency> by lazy { subscriptionCurrencyPublisher }
|
val observableSubscriptionCurrency: Observable<Currency> by lazy { subscriptionCurrencyPublisher }
|
||||||
|
@ -76,18 +83,13 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSubscriptionCurrency(currency: Currency) {
|
|
||||||
putString(KEY_SUBSCRIPTION_CURRENCY_CODE, currency.currencyCode)
|
|
||||||
subscriptionCurrencyPublisher.onNext(currency)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setBoostCurrency(currency: Currency) {
|
fun setBoostCurrency(currency: Currency) {
|
||||||
putString(KEY_CURRENCY_CODE_BOOST, currency.currencyCode)
|
putString(KEY_CURRENCY_CODE_BOOST, currency.currencyCode)
|
||||||
boostCurrencyPublisher.onNext(currency)
|
boostCurrencyPublisher.onNext(currency)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSubscriber(): Subscriber? {
|
fun getSubscriber(currency: Currency): Subscriber? {
|
||||||
val currencyCode = getSubscriptionCurrency().currencyCode
|
val currencyCode = currency.currencyCode
|
||||||
val subscriberIdBytes = getBlob("$KEY_SUBSCRIBER_ID_PREFIX$currencyCode", null)
|
val subscriberIdBytes = getBlob("$KEY_SUBSCRIBER_ID_PREFIX$currencyCode", null)
|
||||||
|
|
||||||
return if (subscriberIdBytes == null) {
|
return if (subscriberIdBytes == null) {
|
||||||
|
@ -97,13 +99,22 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSubscriber(): Subscriber? {
|
||||||
|
return getSubscriber(getSubscriptionCurrency())
|
||||||
|
}
|
||||||
|
|
||||||
fun requireSubscriber(): Subscriber {
|
fun requireSubscriber(): Subscriber {
|
||||||
return getSubscriber() ?: throw Exception("Subscriber ID is not set.")
|
return getSubscriber() ?: throw Exception("Subscriber ID is not set.")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSubscriber(subscriber: Subscriber) {
|
fun setSubscriber(subscriber: Subscriber) {
|
||||||
val currencyCode = subscriber.currencyCode
|
val currencyCode = subscriber.currencyCode
|
||||||
putBlob("$KEY_SUBSCRIBER_ID_PREFIX$currencyCode", subscriber.subscriberId.bytes)
|
store.beginWrite()
|
||||||
|
.putBlob("$KEY_SUBSCRIBER_ID_PREFIX$currencyCode", subscriber.subscriberId.bytes)
|
||||||
|
.putString(KEY_SUBSCRIPTION_CURRENCY_CODE, currencyCode)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
subscriptionCurrencyPublisher.onNext(Currency.getInstance(currencyCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLevelOperation(): LevelUpdateOperation? {
|
fun getLevelOperation(): LevelUpdateOperation? {
|
||||||
|
@ -133,6 +144,20 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setExpiredBadge(badge: Badge?) {
|
||||||
|
if (badge != null) {
|
||||||
|
putBlob(EXPIRED_BADGE, Badges.toDatabaseBadge(badge).toByteArray())
|
||||||
|
} else {
|
||||||
|
remove(EXPIRED_BADGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExpiredBadge(): Badge? {
|
||||||
|
val badgeBytes = getBlob(EXPIRED_BADGE, null) ?: return null
|
||||||
|
|
||||||
|
return Badges.fromDatabaseBadge(BadgeList.Badge.parseFrom(badgeBytes))
|
||||||
|
}
|
||||||
|
|
||||||
private fun clearLevelOperation() {
|
private fun clearLevelOperation() {
|
||||||
remove(KEY_IDEMPOTENCY)
|
remove(KEY_IDEMPOTENCY)
|
||||||
remove(KEY_LEVEL)
|
remove(KEY_LEVEL)
|
||||||
|
|
|
@ -108,6 +108,7 @@ public final class Megaphones {
|
||||||
put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER);
|
put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER);
|
||||||
put(Event.CHAT_COLORS, ALWAYS);
|
put(Event.CHAT_COLORS, ALWAYS);
|
||||||
put(Event.ADD_A_PROFILE_PHOTO, shouldShowAddAProfilePhotoMegaphone(context) ? ALWAYS : NEVER);
|
put(Event.ADD_A_PROFILE_PHOTO, shouldShowAddAProfilePhotoMegaphone(context) ? ALWAYS : NEVER);
|
||||||
|
put(Event.BECOME_A_SUSTAINER, shouldShowBecomeASustainerMegaphone() ? ALWAYS : NEVER);
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,6 +140,8 @@ public final class Megaphones {
|
||||||
return buildChatColorsMegaphone(context);
|
return buildChatColorsMegaphone(context);
|
||||||
case ADD_A_PROFILE_PHOTO:
|
case ADD_A_PROFILE_PHOTO:
|
||||||
return buildAddAProfilePhotoMegaphone(context);
|
return buildAddAProfilePhotoMegaphone(context);
|
||||||
|
case BECOME_A_SUSTAINER:
|
||||||
|
return buildBecomeASustainerMegaphone(context);
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Event not handled!");
|
throw new IllegalArgumentException("Event not handled!");
|
||||||
}
|
}
|
||||||
|
@ -340,6 +343,22 @@ public final class Megaphones {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static @NonNull Megaphone buildBecomeASustainerMegaphone(@NonNull Context context) {
|
||||||
|
return new Megaphone.Builder(Event.BECOME_A_SUSTAINER, Megaphone.Style.BASIC)
|
||||||
|
.setTitle(R.string.BecomeASustainerMegaphone__become_a_sustainer)
|
||||||
|
.setImage(R.drawable.ic_become_a_sustainer_megaphone)
|
||||||
|
.setBody(R.string.BecomeASustainerMegaphone__signal_is_powered)
|
||||||
|
.setActionButton(R.string.BecomeASustainerMegaphone__donate, (megaphone, listener) -> {
|
||||||
|
listener.onMegaphoneNavigationRequested(AppSettingsActivity.subscriptions(context));
|
||||||
|
listener.onMegaphoneCompleted(Event.BECOME_A_SUSTAINER);
|
||||||
|
})
|
||||||
|
.setSecondaryButton(R.string.BecomeASustainerMegaphone__no_thanks, (megaphone, listener) -> {
|
||||||
|
listener.onMegaphoneCompleted(Event.BECOME_A_SUSTAINER);
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static boolean shouldShowMessageRequestsMegaphone() {
|
private static boolean shouldShowMessageRequestsMegaphone() {
|
||||||
return Recipient.self().getProfileName() == ProfileName.EMPTY;
|
return Recipient.self().getProfileName() == ProfileName.EMPTY;
|
||||||
}
|
}
|
||||||
|
@ -364,6 +383,10 @@ public final class Megaphones {
|
||||||
return SignalStore.onboarding().hasOnboarding(context);
|
return SignalStore.onboarding().hasOnboarding(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean shouldShowBecomeASustainerMegaphone() {
|
||||||
|
return FeatureFlags.donorBadges();
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean shouldShowNotificationsMegaphone(@NonNull Context context) {
|
private static boolean shouldShowNotificationsMegaphone(@NonNull Context context) {
|
||||||
boolean shouldShow = !SignalStore.settings().isMessageNotificationsEnabled() ||
|
boolean shouldShow = !SignalStore.settings().isMessageNotificationsEnabled() ||
|
||||||
!NotificationChannels.isMessageChannelEnabled(context) ||
|
!NotificationChannels.isMessageChannelEnabled(context) ||
|
||||||
|
@ -410,7 +433,8 @@ public final class Megaphones {
|
||||||
ONBOARDING("onboarding"),
|
ONBOARDING("onboarding"),
|
||||||
NOTIFICATIONS("notifications"),
|
NOTIFICATIONS("notifications"),
|
||||||
CHAT_COLORS("chat_colors"),
|
CHAT_COLORS("chat_colors"),
|
||||||
ADD_A_PROFILE_PHOTO("add_a_profile_photo");
|
ADD_A_PROFILE_PHOTO("add_a_profile_photo"),
|
||||||
|
BECOME_A_SUSTAINER("become_a_sustainer");
|
||||||
|
|
||||||
private final String key;
|
private final String key;
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2,7 +2,7 @@
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:state_selected="true">
|
<item android:state_selected="true">
|
||||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<stroke android:color="@color/signal_accent_primary" android:width="1dp" />
|
<stroke android:color="@color/signal_accent_primary" android:width="2dp" />
|
||||||
<corners android:radius="10dp" />
|
<corners android:radius="10dp" />
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
@ -3,15 +3,12 @@
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
app:cardCornerRadius="0dp"
|
app:cardCornerRadius="0dp"
|
||||||
app:cardElevation="0dp">
|
app:cardElevation="0dp">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:layout_marginTop="24dp"
|
|
||||||
android:layout_marginBottom="24dp">
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
android:id="@+id/avatar"
|
android:id="@+id/avatar"
|
||||||
|
|
|
@ -6,26 +6,6 @@
|
||||||
|
|
||||||
<include layout="@layout/dsl_settings_toolbar" />
|
<include layout="@layout/dsl_settings_toolbar" />
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/section_header_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minHeight="48dp"
|
|
||||||
android:paddingStart="@dimen/dsl_settings_gutter"
|
|
||||||
android:paddingTop="16dp"
|
|
||||||
android:paddingEnd="@dimen/dsl_settings_gutter"
|
|
||||||
android:paddingBottom="12dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/section_header"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/SelectFeaturedBadgeFragment__preview"
|
|
||||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/preview"
|
android:id="@+id/preview"
|
||||||
layout="@layout/featured_badge_preview_preference"
|
layout="@layout/featured_badge_preview_preference"
|
||||||
|
@ -33,7 +13,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/section_header_container" />
|
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/scroll_shadow"
|
android:id="@+id/scroll_shadow"
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/circled_rectangle_outline"
|
android:background="@drawable/circled_rectangle_outline"
|
||||||
android:drawablePadding="8dp"
|
android:drawablePadding="8dp"
|
||||||
|
android:minHeight="32dp"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
app:drawableEndCompat="@drawable/ic_chevron_down_20"
|
app:drawableEndCompat="@drawable/ic_chevron_down_20"
|
||||||
app:drawableTint="@color/conversation_mention_background_color"
|
app:drawableTint="@color/conversation_mention_background_color"
|
||||||
|
|
|
@ -9,8 +9,7 @@
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginBottom="8dp">
|
||||||
android:layout_marginBottom="24dp">
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
android:id="@+id/avatar"
|
android:id="@+id/avatar"
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="12dp"
|
||||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
android:background="@drawable/selectable_rounded_outline"
|
android:background="@drawable/selectable_rounded_outline"
|
||||||
android:padding="16dp">
|
android:padding="16dp">
|
||||||
|
|
|
@ -21,6 +21,22 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp" />
|
android:layout_marginTop="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/no_support"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||||
|
android:text="@string/ViewBadgeBottomSheetDialogFragment__your_device_doesn_t_support_google_pay_so_you_can_t_subscribe_to_earn_a_badge_you_can_still_support_signal_by_making_a_donation_on_our_website"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||||
|
android:textColor="@color/signal_text_secondary"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tab_layout" />
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/tab_layout"
|
android:id="@+id/tab_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -41,8 +57,8 @@
|
||||||
android:layout_marginBottom="32dp"
|
android:layout_marginBottom="32dp"
|
||||||
android:minHeight="48dp"
|
android:minHeight="48dp"
|
||||||
android:text="@string/ViewBadgeBottomSheetDialogFragment__become_a_sustainer"
|
android:text="@string/ViewBadgeBottomSheetDialogFragment__become_a_sustainer"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:iconGravity="textEnd"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:iconTint="@color/white"
|
||||||
app:layout_constraintTop_toBottomOf="@id/description" />
|
tools:icon="@drawable/ic_open_20" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -3722,6 +3722,12 @@
|
||||||
<string name="AddAProfilePhotoMegaphone__not_now">Not now</string>
|
<string name="AddAProfilePhotoMegaphone__not_now">Not now</string>
|
||||||
<string name="AddAProfilePhotoMegaphone__add_photo">Add photo</string>
|
<string name="AddAProfilePhotoMegaphone__add_photo">Add photo</string>
|
||||||
|
|
||||||
|
<!-- BecomeASustainerMegaphone -->
|
||||||
|
<string name="BecomeASustainerMegaphone__become_a_sustainer">Become a Sustainer</string>
|
||||||
|
<string name="BecomeASustainerMegaphone__signal_is_powered">Signal is powered by people like you. Donate and receive a profile badge.</string>
|
||||||
|
<string name="BecomeASustainerMegaphone__no_thanks">No thanks</string>
|
||||||
|
<string name="BecomeASustainerMegaphone__donate">Donate</string>
|
||||||
|
|
||||||
<!-- KeyboardPagerFragment -->
|
<!-- KeyboardPagerFragment -->
|
||||||
<string name="KeyboardPagerFragment_emoji">Emoji</string>
|
<string name="KeyboardPagerFragment_emoji">Emoji</string>
|
||||||
<string name="KeyboardPagerFragment_open_emoji_search">Open emoji search</string>
|
<string name="KeyboardPagerFragment_open_emoji_search">Open emoji search</string>
|
||||||
|
@ -3972,6 +3978,10 @@
|
||||||
<string name="ExpiredBadgeBottomSheetDialogFragment__to_continue_supporting">To continue supporting technology that is built for you—not for your data—please consider becoming a monthly subscriber.</string>
|
<string name="ExpiredBadgeBottomSheetDialogFragment__to_continue_supporting">To continue supporting technology that is built for you—not for your data—please consider becoming a monthly subscriber.</string>
|
||||||
<string name="ExpiredBadgeBottomSheetDialogFragment__become_a_subscriber">Become a subscriber</string>
|
<string name="ExpiredBadgeBottomSheetDialogFragment__become_a_subscriber">Become a subscriber</string>
|
||||||
<string name="ExpiredBadgeBottomSheetDialogFragment__not_now">Not now</string>
|
<string name="ExpiredBadgeBottomSheetDialogFragment__not_now">Not now</string>
|
||||||
|
<string name="ExpiredBadgeBottomSheetDialogFragment__your_subscription_was_cancelled">Your subscription was cancelled.</string>
|
||||||
|
<string name="ExpiredBadgeBottomSheetDialogFragment__because">Because you were inactive for more than 45 days, your subscription to Signal has been automatically cancelled.</string>
|
||||||
|
<string name="ExpiredBadgeBottomSheetDialogFragment__to_continue_supporting_signal">To continue supporting Signal and to reactivate your badge, renew now.</string>
|
||||||
|
<string name="ExpiredBadgeBottomSheetDialogFragment__renew_subscription">Renew subscription</string>
|
||||||
|
|
||||||
<string name="Subscription__verification_failed">Subscription Verification Failed</string>
|
<string name="Subscription__verification_failed">Subscription Verification Failed</string>
|
||||||
<string name="Subscription__please_contact_support_for_more_information">Please contact support for more information.</string>
|
<string name="Subscription__please_contact_support_for_more_information">Please contact support for more information.</string>
|
||||||
|
@ -3987,6 +3997,7 @@
|
||||||
<string name="DonationsErrors__you_have_to_set_up_google_pay_to_donate_in_app">You have to set up Google Pay to donate in-app.</string>
|
<string name="DonationsErrors__you_have_to_set_up_google_pay_to_donate_in_app">You have to set up Google Pay to donate in-app.</string>
|
||||||
<string name="DonationsErrors__failed_to_cancel_subscription">Failed to cancel subscription</string>
|
<string name="DonationsErrors__failed_to_cancel_subscription">Failed to cancel subscription</string>
|
||||||
<string name="DonationsErrors__subscription_cancellation_requires_an_internet_connection">Subscription cancellation requires an internet connection.</string>
|
<string name="DonationsErrors__subscription_cancellation_requires_an_internet_connection">Subscription cancellation requires an internet connection.</string>
|
||||||
|
<string name="ViewBadgeBottomSheetDialogFragment__your_device_doesn_t_support_google_pay_so_you_can_t_subscribe_to_earn_a_badge_you_can_still_support_signal_by_making_a_donation_on_our_website">Your device doesn\'t support Google Pay, so you can\'t subscribe to earn a badge. You can still support Signal by making a donation on our website.</string>
|
||||||
|
|
||||||
<!-- EOF -->
|
<!-- EOF -->
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue