Implement several pieces of UI polish for badges.

This commit is contained in:
Alex Hart
2021-10-28 13:59:05 -03:00
committed by Greyson Parrelli
parent 186bd9db48
commit 755ec672c0
33 changed files with 364 additions and 113 deletions

View File

@@ -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<Uri, String> {
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<Uri, String> = getBestBadgeImageUriForDevice(serviceBadge)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<BadgeList.Badge> 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);

View File

@@ -59,16 +59,18 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
);
}
public static Pair<String, String> 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,

View File

@@ -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<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)
.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<RefreshOwnProfileJob> {
@Override

View File

@@ -64,17 +64,19 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
);
}
public static Pair<String, String> 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,

View File

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

View File

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