diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/Badges.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/Badges.kt index f19aac8555..9223028792 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/Badges.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/Badges.kt @@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode import org.thoughtcrime.securesms.components.settings.DSLConfiguration +import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList import org.thoughtcrime.securesms.util.ScreenDensity import org.whispersystems.libsignal.util.Pair import org.whispersystems.signalservice.api.profiles.SignalServiceProfile @@ -59,8 +60,7 @@ object Badges { } private fun getBestBadgeImageUriForDevice(serviceBadge: SignalServiceProfile.Badge): Pair { - val bestDensity = ScreenDensity.getBestDensityBucketForDevice() - return when (bestDensity) { + return when (ScreenDensity.getBestDensityBucketForDevice()) { "ldpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[0]), "ldpi") "mdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[1]), "mdpi") "hdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[2]), "hdpi") @@ -74,6 +74,34 @@ object Badges { 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 fun fromServiceBadge(serviceBadge: SignalServiceProfile.Badge): Badge { val uriAndDensity: Pair = getBestBadgeImageUriForDevice(serviceBadge) diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt index 3e35a1b0df..0c7315a6cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt @@ -37,6 +37,7 @@ data class Badge( ) : Parcelable, Key { fun isExpired(): Boolean = expirationTimestamp < System.currentTimeMillis() + fun isBoost(): Boolean = id == BOOST_BADGE_ID override fun updateDiskCacheKey(messageDigest: MessageDigest) { messageDigest.update(id.toByteArray(Key.CHARSET)) @@ -159,6 +160,8 @@ data class Badge( } companion object { + const val BOOST_BADGE_ID = "BOOST" + private val SELECTION_CHANGED = Any() fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt index 6b727365d3..2e4f248875 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/expired/ExpiredBadgeBottomSheetDialogFragment.kt @@ -1,14 +1,17 @@ package org.thoughtcrime.securesms.badges.self.expired +import androidx.fragment.app.FragmentManager import androidx.navigation.fragment.findNavController import org.signal.core.util.DimensionUnit import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.models.ExpiredBadge import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsText 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. @@ -28,13 +31,23 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment( return configure { 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()) noPadTextPref( 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 ) ) @@ -43,7 +56,11 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment( noPadTextPref( 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 ) ) @@ -51,7 +68,13 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment( space(DimensionUnit.DP.toPixels(92f).toInt()) 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 = { dismiss() 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) + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/view/ViewBadgeBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/view/ViewBadgeBottomSheetDialogFragment.kt index bf408cb044..f139377164 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/view/ViewBadgeBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/view/ViewBadgeBottomSheetDialogFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels 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.LargeBadge 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.RecipientId import org.thoughtcrime.securesms.util.BottomSheetUtil +import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.MappingAdapter +import org.thoughtcrime.securesms.util.PlayServicesUtil import org.thoughtcrime.securesms.util.visible class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() { @@ -37,11 +41,25 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr val pager: ViewPager2 = view.findViewById(R.id.pager) val tabs: TabLayout = view.findViewById(R.id.tab_layout) val action: MaterialButton = view.findViewById(R.id.action) + val noSupport: View = view.findViewById(R.id.no_support) if (getRecipientId() == Recipient.self().id) { 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() LargeBadge.register(adapter) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsText.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsText.kt index 6f80910125..f707975edb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsText.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsText.kt @@ -70,4 +70,10 @@ sealed class DSLSettingsText { return SpanUtil.textAppearance(context, textAppearance, charSequence) } } + + object BoldModifier : Modifier { + override fun modify(context: Context, charSequence: CharSequence): CharSequence { + return SpanUtil.bold(charSequence) + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt index 3e79628c3e..e4439b21ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt @@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.MappingAdapter import org.thoughtcrime.securesms.util.MappingViewHolder +import org.thoughtcrime.securesms.util.PlayServicesUtil 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( SubscriptionPreference( title = DSLSettingsText.from(R.string.preferences__subscription), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt index 641fdcf381..2e69789544 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/DonationPaymentRepository.kt @@ -106,15 +106,10 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet return Completable.create { stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), paymentIntent).blockingSubscribe() - val jobIds = BoostReceiptRequestResponseJob.enqueueChain(paymentIntent) - val countDownLatch = CountDownLatch(2) + val jobId = BoostReceiptRequestResponseJob.enqueueChain(paymentIntent) + val countDownLatch = CountDownLatch(1) - ApplicationDependencies.getJobManager().addListener(jobIds.first()) { _, jobState -> - if (jobState.isComplete) { - countDownLatch.countDown() - } - } - ApplicationDependencies.getJobManager().addListener(jobIds.second()) { _, jobState -> + ApplicationDependencies.getJobManager().addListener(jobId) { _, jobState -> if (jobState.isComplete) { countDownLatch.countDown() } @@ -146,15 +141,10 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet SignalStore.donationsValues().clearLevelOperation(levelUpdateOperation) it.onComplete() }.andThen { - val jobIds = SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation() - val countDownLatch = CountDownLatch(2) + val jobId = SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation() + val countDownLatch = CountDownLatch(1) - ApplicationDependencies.getJobManager().addListener(jobIds.first()) { _, jobState -> - if (jobState.isComplete) { - countDownLatch.countDown() - } - } - ApplicationDependencies.getJobManager().addListener(jobIds.second()) { _, jobState -> + ApplicationDependencies.getJobManager().addListener(jobId) { _, jobState -> if (jobState.isComplete) { countDownLatch.countDown() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/Boost.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/Boost.kt index 0d5cb9ea9f..a2127d5831 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/Boost.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/Boost.kt @@ -119,6 +119,9 @@ data class Boost( if (model.isCustomAmountFocused && !custom.hasFocus()) { ViewUtil.focusAndShowKeyboard(custom) + } else if (!model.isCustomAmountFocused && custom.hasFocus()) { + ViewUtil.hideKeyboard(context, custom) + custom.clearFocus() } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostFragment.kt index b9725dbbab..d7d5192723 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostFragment.kt @@ -146,7 +146,7 @@ class BoostFragment : DSLSettingsBottomSheetFragment( text = DSLSettingsText.from(R.string.SubscribeFragment__more_payment_options), icon = DSLSettingsIcon.from(R.drawable.ic_open_20, R.color.signal_accent_primary), onClick = { - // TODO + // TODO [alex] -- Where's this go? } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt index c7569f2a9c..7e44d6c960 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyFragment.kt @@ -28,12 +28,12 @@ class SetCurrencyFragment : DSLSettingsBottomSheetFragment() { private fun getConfiguration(state: SetCurrencyState): DSLConfiguration { return configure { state.currencies.forEach { currency -> - radioPref( + clickPref( title = DSLSettingsText.from(currency.getDisplayName(Locale.getDefault())), summary = DSLSettingsText.from(currency.currencyCode), - isChecked = currency.currencyCode == state.selectedCurrencyCode, onClick = { viewModel.setSelectedCurrency(currency.currencyCode) + dismissAllowingStateLoss() } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyViewModel.kt index b36497b0cf..7f452b37a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/currency/SetCurrencyViewModel.kt @@ -7,7 +7,10 @@ import androidx.lifecycle.ViewModelProvider import org.signal.donations.StripeApi import org.thoughtcrime.securesms.BuildConfig 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.whispersystems.signalservice.api.subscriptions.SubscriberId import java.util.Currency import java.util.Locale @@ -43,7 +46,21 @@ class SetCurrencyViewModel(private val isBoost: Boolean) : ViewModel() { if (isBoost) { SignalStore.donationsValues().setBoostCurrency(Currency.getInstance(selectedCurrencyCode)) } 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() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt index 644bf1223c..35bcd29674 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/manage/ManageDonationsFragment.kt @@ -7,6 +7,7 @@ import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController import org.signal.core.util.DimensionUnit 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.DSLSettingsAdapter import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment @@ -59,6 +60,7 @@ class ManageDonationsFragment : DSLSettingsFragment() { ActiveSubscriptionPreference.register(adapter) IndeterminateLoadingCircle.register(adapter) + BadgePreview.register(adapter) viewModel.state.observe(viewLifecycleOwner) { state -> adapter.submitList(getConfiguration(state).toMappingModelList()) @@ -75,6 +77,14 @@ class ManageDonationsFragment : DSLSettingsFragment() { private fun getConfiguration(state: ManageDonationsState): DSLConfiguration { return configure { + customPref( + BadgePreview.Model( + badge = state.featuredBadge + ) + ) + + space(DimensionUnit.DP.toPixels(8f).toInt()) + sectionHeaderPref( title = DSLSettingsText.from( R.string.SubscribeFragment__signal_is_powered_by_people_like_you, @@ -82,10 +92,12 @@ class ManageDonationsFragment : DSLSettingsFragment() { ) ) + space(DimensionUnit.DP.toPixels(32f).toInt()) + noPadTextPref( title = DSLSettingsText.from( R.string.ManageDonationsFragment__my_support, - DSLSettingsText.Title2BoldModifier + DSLSettingsText.Body1BoldModifier, DSLSettingsText.BoldModifier ) ) @@ -94,7 +106,7 @@ class ManageDonationsFragment : DSLSettingsFragment() { if (activeSubscription.isActive) { val subscription: Subscription? = state.availableSubscriptions.firstOrNull { activeSubscription.activeSubscription.level == it.level } if (subscription != null) { - space(DimensionUnit.DP.toPixels(16f).toInt()) + space(DimensionUnit.DP.toPixels(12f).toInt()) customPref( ActiveSubscriptionPreference.Model( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeFragment.kt index 117b46fe25..26cdb1649d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeFragment.kt @@ -124,6 +124,8 @@ class SubscribeFragment : DSLSettingsFragment( ) ) + space(DimensionUnit.DP.toPixels(4f).toInt()) + state.subscriptions.forEach { val isActive = state.activeSubscription?.activeSubscription?.level == it.level customPref( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/AvatarPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/AvatarPreference.kt index ea137b6387..aad9a87d04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/AvatarPreference.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/AvatarPreference.kt @@ -49,13 +49,19 @@ object AvatarPreference { } override fun bind(model: Model) { - badge.setBadgeFromRecipient(model.recipient) - badge.setOnClickListener { - val badge = model.recipient.badges.firstOrNull() - if (badge != null) { - model.onBadgeClick(badge) + if (model.recipient.isSelf) { + badge.setBadge(null) + badge.setOnClickListener(null) + } else { + badge.setBadgeFromRecipient(model.recipient) + badge.setOnClickListener { + val badge = model.recipient.badges.firstOrNull() + if (badge != null) { + model.onBadgeClick(badge) + } } } + avatar.setAvatar(model.recipient) avatar.disableQuickContact() avatar.setOnClickListener { model.onAvatarClick(avatar) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java index ff5414b4b9..f20389f98f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java @@ -121,7 +121,11 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi 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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java index bc14512281..9f01e19baa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java @@ -57,7 +57,11 @@ public class ConversationBannerView extends ConstraintLayout { } 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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java index 2d0b85c415..a4370930b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java @@ -106,7 +106,11 @@ public class ConversationTitleView extends RelativeLayout { this.avatar.setAvatar(glideRequests, recipient, false); } - badge.setBadgeFromRecipient(recipient); + if (recipient == null || recipient.isSelf()) { + badge.setBadgeFromRecipient(null); + } else { + badge.setBadgeFromRecipient(recipient); + } updateVerifiedSubtitleVisibility(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 1f740e992b..2948bd80aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -77,6 +77,8 @@ import org.thoughtcrime.securesms.MainNavigator; import org.thoughtcrime.securesms.MuteDialog; import org.thoughtcrime.securesms.NewConversationActivity; 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.SearchToolbar; import org.thoughtcrime.securesms.components.menu.ActionItem; @@ -322,6 +324,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode Log.i(TAG, "Recaptcha required."); RecaptchaProofBottomSheetFragment.show(getChildFragmentManager()); } + + Badge expiredBadge = SignalStore.donationsValues().getExpiredBadge(); + if (expiredBadge != null) { + SignalStore.donationsValues().setExpiredBadge(null); + ExpiredBadgeBottomSheetDialogFragment.show(expiredBadge, getParentFragmentManager()); + } } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 67e7a7319d..61cbf922a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -23,6 +23,7 @@ import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.thoughtcrime.securesms.badges.Badges; import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.conversation.colors.AvatarColor; @@ -1370,16 +1371,7 @@ public class RecipientDatabase extends Database { List protoBadges = badgeList.getBadgesList(); badges = new ArrayList<>(protoBadges.size()); for (BadgeList.Badge protoBadge : protoBadges) { - badges.add(new Badge( - protoBadge.getId(), - Badge.Category.Companion.fromCode(protoBadge.getCategory()), - protoBadge.getName(), - protoBadge.getDescription(), - Uri.parse(protoBadge.getImageUrl()), - protoBadge.getImageDensity(), - protoBadge.getExpiration(), - protoBadge.getVisible() - )); + badges.add(Badges.fromDatabaseBadge(protoBadge)); } } else { badges = Collections.emptyList(); @@ -1712,15 +1704,7 @@ public class RecipientDatabase extends Database { BadgeList.Builder badgeListBuilder = BadgeList.newBuilder(); for (final Badge badge : badges) { - badgeListBuilder.addBadges(BadgeList.Badge.newBuilder() - .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())); + badgeListBuilder.addBadges(Badges.toDatabaseBadge(badge)); } ContentValues values = new ContentValues(1); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java index 990a8cd985..d728bc4201 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java @@ -59,16 +59,18 @@ public class BoostReceiptRequestResponseJob extends BaseJob { ); } - public static Pair enqueueChain(StripeApi.PaymentIntent paymentIntent) { - BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntent); - DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost(); + public static String enqueueChain(StripeApi.PaymentIntent paymentIntent) { + BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntent); + DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost(); + RefreshOwnProfileJob refreshOwnProfileJob = new RefreshOwnProfileJob(); ApplicationDependencies.getJobManager() .startChain(requestReceiptJob) .then(redeemReceiptJob) + .then(refreshOwnProfileJob) .enqueue(); - return new Pair<>(requestReceiptJob.getId(), redeemReceiptJob.getId()); + return refreshOwnProfileJob.getId(); } private BoostReceiptRequestResponseJob(@NonNull Parameters parameters, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index 4a3cf8de66..9e9b5f13be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.jobs; -import android.net.Uri; import android.text.TextUtils; import androidx.annotation.NonNull; @@ -9,7 +8,6 @@ import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; -import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.badges.Badges; import org.thoughtcrime.securesms.badges.models.Badge; 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.recipients.Recipient; import org.thoughtcrime.securesms.util.ProfileUtil; -import org.thoughtcrime.securesms.util.ScreenDensity; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; -import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; 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 java.io.IOException; -import java.math.BigDecimal; -import java.sql.Timestamp; +import java.util.Comparator; import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; @@ -175,11 +172,60 @@ public class RefreshOwnProfileJob extends BaseJob { return; } + Set localDonorBadgeIds = Recipient.self() + .getBadges() + .stream() + .filter(badge -> badge.getCategory() == Badge.Category.Donor) + .map(Badge::getId) + .collect(Collectors.toSet()); + + Set 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) .setBadges(Recipient.self().getId(), 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 { @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java index 3028b7f084..ad36250a42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java @@ -64,17 +64,19 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { ); } - public static Pair enqueueSubscriptionContinuation() { - Subscriber subscriber = SignalStore.donationsValues().requireSubscriber(); - SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId()); - DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription(); + public static String enqueueSubscriptionContinuation() { + Subscriber subscriber = SignalStore.donationsValues().requireSubscriber(); + SubscriptionReceiptRequestResponseJob requestReceiptJob = createJob(subscriber.getSubscriberId()); + DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForSubscription(); + RefreshOwnProfileJob refreshOwnProfileJob = new RefreshOwnProfileJob(); ApplicationDependencies.getJobManager() .startChain(requestReceiptJob) .then(redeemReceiptJob) + .then(refreshOwnProfileJob) .enqueue(); - return new Pair<>(requestReceiptJob.getId(), redeemReceiptJob.getId()); + return refreshOwnProfileJob.getId(); } private SubscriptionReceiptRequestResponseJob(@NonNull Parameters parameters, diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt index 5a3dca7c70..57487c6e5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/DonationsValues.kt @@ -4,6 +4,9 @@ import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.subjects.BehaviorSubject import io.reactivex.rxjava3.subjects.Subject 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.payments.currency.CurrencyUtil 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_LAST_KEEP_ALIVE_LAUNCH = "donation.last.successful.ping" 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 getKeysToIncludeInBackup(): MutableList = mutableListOf(KEY_SUBSCRIPTION_CURRENCY_CODE, KEY_LAST_KEEP_ALIVE_LAUNCH) + override fun getKeysToIncludeInBackup(): MutableList = mutableListOf( + KEY_CURRENCY_CODE_BOOST, + KEY_LAST_KEEP_ALIVE_LAUNCH + ) private val subscriptionCurrencyPublisher: Subject by lazy { BehaviorSubject.createDefault(getSubscriptionCurrency()) } val observableSubscriptionCurrency: Observable 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) { putString(KEY_CURRENCY_CODE_BOOST, currency.currencyCode) boostCurrencyPublisher.onNext(currency) } - fun getSubscriber(): Subscriber? { - val currencyCode = getSubscriptionCurrency().currencyCode + fun getSubscriber(currency: Currency): Subscriber? { + val currencyCode = currency.currencyCode val subscriberIdBytes = getBlob("$KEY_SUBSCRIBER_ID_PREFIX$currencyCode", 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 { return getSubscriber() ?: throw Exception("Subscriber ID is not set.") } fun setSubscriber(subscriber: Subscriber) { 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? { @@ -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() { remove(KEY_IDEMPOTENCY) remove(KEY_LEVEL) diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index f2c87cae2b..b7ab172912 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -108,6 +108,7 @@ public final class Megaphones { put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER); put(Event.CHAT_COLORS, ALWAYS); 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); case ADD_A_PROFILE_PHOTO: return buildAddAProfilePhotoMegaphone(context); + case BECOME_A_SUSTAINER: + return buildBecomeASustainerMegaphone(context); default: throw new IllegalArgumentException("Event not handled!"); } @@ -340,6 +343,22 @@ public final class Megaphones { .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() { return Recipient.self().getProfileName() == ProfileName.EMPTY; } @@ -364,6 +383,10 @@ public final class Megaphones { return SignalStore.onboarding().hasOnboarding(context); } + private static boolean shouldShowBecomeASustainerMegaphone() { + return FeatureFlags.donorBadges(); + } + private static boolean shouldShowNotificationsMegaphone(@NonNull Context context) { boolean shouldShow = !SignalStore.settings().isMessageNotificationsEnabled() || !NotificationChannels.isMessageChannelEnabled(context) || @@ -410,7 +433,8 @@ public final class Megaphones { ONBOARDING("onboarding"), NOTIFICATIONS("notifications"), 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; diff --git a/app/src/main/res/drawable/ic_become_a_sustainer_megaphone.xml b/app/src/main/res/drawable/ic_become_a_sustainer_megaphone.xml new file mode 100644 index 0000000000..8aa01e46b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_become_a_sustainer_megaphone.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/selectable_rounded_outline.xml b/app/src/main/res/drawable/selectable_rounded_outline.xml index 4eff28f1fd..8d9c699935 100644 --- a/app/src/main/res/drawable/selectable_rounded_outline.xml +++ b/app/src/main/res/drawable/selectable_rounded_outline.xml @@ -2,7 +2,7 @@ - + diff --git a/app/src/main/res/layout/featured_badge_preview_preference.xml b/app/src/main/res/layout/featured_badge_preview_preference.xml index e805a4dff3..2ffca91656 100644 --- a/app/src/main/res/layout/featured_badge_preview_preference.xml +++ b/app/src/main/res/layout/featured_badge_preview_preference.xml @@ -3,15 +3,12 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="16dp" app:cardCornerRadius="0dp" app:cardElevation="0dp"> + android:layout_height="match_parent"> - - - - - + app:layout_constraintTop_toBottomOf="@id/toolbar" /> + android:layout_marginBottom="8dp"> diff --git a/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment.xml b/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment.xml index e041e01c0f..ae7c411f22 100644 --- a/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment.xml +++ b/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment.xml @@ -21,6 +21,22 @@ android:layout_height="wrap_content" android:layout_marginTop="12dp" /> + + + app:iconGravity="textEnd" + app:iconTint="@color/white" + tools:icon="@drawable/ic_open_20" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a7f86f0ac..108d9e74a2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3722,6 +3722,12 @@ Not now Add photo + + Become a Sustainer + Signal is powered by people like you. Donate and receive a profile badge. + No thanks + Donate + Emoji Open emoji search @@ -3972,6 +3978,10 @@ To continue supporting technology that is built for you—not for your data—please consider becoming a monthly subscriber. Become a subscriber Not now + Your subscription was cancelled. + Because you were inactive for more than 45 days, your subscription to Signal has been automatically cancelled. + To continue supporting Signal and to reactivate your badge, renew now. + Renew subscription Subscription Verification Failed Please contact support for more information. @@ -3987,6 +3997,7 @@ You have to set up Google Pay to donate in-app. Failed to cancel subscription Subscription cancellation requires an internet connection. + 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.