From ca24682366d5afc7d3d7a76c4df8aef5ca09fe26 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 11 Nov 2021 13:46:38 -0400 Subject: [PATCH] Fix a bunch UX bugs for donor badges. --- .../securesms/badges/BadgeImageView.kt | 9 +- .../self/overview/BadgesOverviewFragment.kt | 3 +- .../self/overview/BadgesOverviewState.kt | 2 +- .../self/overview/BadgesOverviewViewModel.kt | 1 + .../components/ConversationTypingView.java | 16 ++++ .../components/settings/DSLSettingsAdapter.kt | 2 + .../app/internal/InternalSettingsFragment.kt | 2 +- .../subscription/DonationPaymentRepository.kt | 10 +-- .../settings/app/subscription/boost/Boost.kt | 17 +++- .../app/subscription/boost/BoostFragment.kt | 5 ++ .../app/subscription/boost/BoostViewModel.kt | 3 +- .../subscribe/SubscribeFragment.kt | 5 ++ ...ForYourSupportBottomSheetDialogFragment.kt | 1 + .../securesms/components/settings/dsl.kt | 12 +++ .../components/settings/models/AsyncSwitch.kt | 54 ++++++++++++ .../webrtc/CallParticipantView.java | 13 +++ ...CallParticipantsListUpdatePopupWindow.java | 4 + .../ConversationListFragment.java | 8 +- .../ConversationListItem.java | 2 + .../securesms/jobmanager/JobManager.java | 8 ++ .../jobs/BoostReceiptRequestResponseJob.java | 14 ++-- .../jobs/DonationReceiptRedemptionJob.java | 6 +- .../jobs/SubscriptionKeepAliveJob.java | 3 +- ...SubscriptionReceiptRequestResponseJob.java | 14 ++-- .../manage/ManageProfileFragment.java | 6 +- .../subscription/SubscriptionNotification.kt | 26 ++++++ .../util/viewholders/RecipientViewHolder.java | 7 ++ .../main/res/layout/call_participant_item.xml | 24 ++++++ .../layout/call_participant_list_update.xml | 12 +++ .../layout/call_participants_list_item.xml | 40 +++++++-- .../res/layout/conversation_list_fragment.xml | 9 ++ .../res/layout/conversation_typing_view.xml | 35 ++++++++ .../dsl_async_switch_preference_item.xml | 83 +++++++++++++++++++ .../mentions_picker_recipient_list_item.xml | 24 +++++- .../res/layout/payments_home_payment_item.xml | 12 +++ ...adge_bottom_sheet_dialog_fragment_page.xml | 4 +- app/src/main/res/values/strings.xml | 1 + 37 files changed, 450 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/models/AsyncSwitch.kt create mode 100644 app/src/main/res/layout/dsl_async_switch_preference_item.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt index 18b2bd5a18..74125ca7eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt @@ -44,8 +44,15 @@ class BadgeImageView @JvmOverloads constructor( fun setBadgeFromRecipient(recipient: Recipient?, glideRequests: GlideRequests) { if (recipient == null || recipient.badges.isEmpty()) { setBadge(null, glideRequests) + } else if (recipient.isSelf) { + val badge = recipient.featuredBadge + if (badge == null || !badge.visible || badge.isExpired()) { + setBadge(null, glideRequests) + } else { + setBadge(badge, glideRequests) + } } else { - setBadge(recipient.badges[0], glideRequests) + setBadge(recipient.featuredBadge, glideRequests) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt index 6bb2ddf25b..b97ffff704 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt @@ -68,10 +68,11 @@ class BadgesOverviewFragment : DSLSettingsFragment( fadedBadgeId = state.fadedBadgeId ) - switchPref( + asyncSwitchPref( title = DSLSettingsText.from(R.string.BadgesOverviewFragment__display_badges_on_profile), isChecked = state.displayBadgesOnProfile, isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges, + isProcessing = state.stage == BadgesOverviewState.Stage.UPDATING_BADGE_DISPLAY_STATE, onClick = { viewModel.setDisplayBadgesOnProfile(!state.displayBadgesOnProfile) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewState.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewState.kt index 4b49ec8700..9eaa5c1bad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewState.kt @@ -15,6 +15,6 @@ data class BadgesOverviewState( enum class Stage { INIT, READY, - UPDATING + UPDATING_BADGE_DISPLAY_STATE } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt index 9eaee389bf..b5a0b9fada 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewViewModel.kt @@ -61,6 +61,7 @@ class BadgesOverviewViewModel( } fun setDisplayBadgesOnProfile(displayBadgesOnProfile: Boolean) { + store.update { it.copy(stage = BadgesOverviewState.Stage.UPDATING_BADGE_DISPLAY_STATE) } disposables += badgeRepository.setVisibilityForAllBadges(displayBadgesOnProfile) .subscribe( { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationTypingView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationTypingView.java index 6aac3726ab..6ce3dc15f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationTypingView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationTypingView.java @@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat; import com.annimon.stream.Stream; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.badges.BadgeImageView; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.libsignal.util.Pair; @@ -26,6 +27,9 @@ public class ConversationTypingView extends ConstraintLayout { private AvatarImageView avatar1; private AvatarImageView avatar2; private AvatarImageView avatar3; + private BadgeImageView badge1; + private BadgeImageView badge2; + private BadgeImageView badge3; private View bubble; private TypingIndicatorView indicator; private TextView typistCount; @@ -41,6 +45,9 @@ public class ConversationTypingView extends ConstraintLayout { avatar1 = findViewById(R.id.typing_avatar_1); avatar2 = findViewById(R.id.typing_avatar_2); avatar3 = findViewById(R.id.typing_avatar_3); + badge1 = findViewById(R.id.typing_badge_1); + badge2 = findViewById(R.id.typing_badge_2); + badge3 = findViewById(R.id.typing_badge_3); typistCount = findViewById(R.id.typing_count); bubble = findViewById(R.id.typing_bubble); indicator = findViewById(R.id.typing_indicator); @@ -55,6 +62,9 @@ public class ConversationTypingView extends ConstraintLayout { avatar1.setVisibility(GONE); avatar2.setVisibility(GONE); avatar3.setVisibility(GONE); + badge1.setVisibility(GONE); + badge2.setVisibility(GONE); + badge3.setVisibility(GONE); typistCount.setVisibility(GONE); if (isGroupThread) { @@ -75,15 +85,21 @@ public class ConversationTypingView extends ConstraintLayout { private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List typists) { avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1); avatar1.setVisibility(VISIBLE); + badge1.setBadgeFromRecipient(typists.get(0), glideRequests); + badge1.setVisibility(VISIBLE); if (typists.size() > 1) { avatar2.setAvatar(glideRequests, typists.get(1), false); avatar2.setVisibility(VISIBLE); + badge2.setBadgeFromRecipient(typists.get(1), glideRequests); + badge2.setVisibility(VISIBLE); } if (typists.size() == 3) { avatar3.setAvatar(glideRequests, typists.get(2), false); avatar3.setVisibility(VISIBLE); + badge3.setBadgeFromRecipient(typists.get(2), glideRequests); + badge3.setVisibility(VISIBLE); } if (typists.size() > 3) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt index 94541318e8..2d387632c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt @@ -13,6 +13,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.switchmaterial.SwitchMaterial import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.settings.models.AsyncSwitch import org.thoughtcrime.securesms.components.settings.models.Button import org.thoughtcrime.securesms.components.settings.models.Space import org.thoughtcrime.securesms.components.settings.models.Text @@ -37,6 +38,7 @@ class DSLSettingsAdapter : MappingAdapter() { Text.register(this) Space.register(this) Button.register(this) + AsyncSwitch.register(this) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index 1adb6c9abf..10e32b29a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -415,6 +415,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter } private fun enqueueSubscriptionRedemption() { - SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation() + SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue() } } 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 78030ec119..c8af199ab9 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 @@ -132,11 +132,10 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet return Completable.create { stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), paymentIntent).blockingSubscribe() - val jobId = BoostReceiptRequestResponseJob.enqueueChain(paymentIntent) val countDownLatch = CountDownLatch(1) - var finalJobState: JobTracker.JobState? = null - ApplicationDependencies.getJobManager().addListener(jobId) { _, jobState -> + + BoostReceiptRequestResponseJob.createJobChain(paymentIntent).enqueue { _, jobState -> if (jobState.isComplete) { finalJobState = jobState countDownLatch.countDown() @@ -200,11 +199,10 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet } }.andThen { Log.d(TAG, "Enqueuing request response job chain.", true) - val jobId = SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation() val countDownLatch = CountDownLatch(1) - var finalJobState: JobTracker.JobState? = null - ApplicationDependencies.getJobManager().addListener(jobId) { _, jobState -> + + SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue { _, jobState -> if (jobState.isComplete) { finalJobState = jobState 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 98417f3cb6..1f86adecdc 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 @@ -11,6 +11,7 @@ import android.view.View import androidx.annotation.VisibleForTesting import androidx.appcompat.widget.AppCompatEditText import androidx.core.animation.doOnEnd +import androidx.core.text.isDigitsOnly import androidx.core.widget.addTextChangedListener import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -25,6 +26,7 @@ import org.thoughtcrime.securesms.util.MappingAdapter import org.thoughtcrime.securesms.util.MappingViewHolder import org.thoughtcrime.securesms.util.ViewUtil import java.lang.Integer.min +import java.text.DecimalFormatSymbols import java.util.Currency import java.util.Locale import java.util.regex.Pattern @@ -137,7 +139,10 @@ data class Boost( button.text = FiatMoneyUtil.format( context.resources, boost.price, - FiatMoneyUtil.formatOptions().trimZerosAfterDecimal() + FiatMoneyUtil + .formatOptions() + .numberOnly() + .trimZerosAfterDecimal() ) button.setOnClickListener { model.onBoostClick(it, boost) @@ -181,11 +186,12 @@ data class Boost( } @VisibleForTesting - class MoneyFilter(val currency: Currency, private val onCustomAmountChanged: (String) -> Unit = {}) : DigitsKeyListener(), TextWatcher { + class MoneyFilter(val currency: Currency, private val onCustomAmountChanged: (String) -> Unit = {}) : DigitsKeyListener(false, true), TextWatcher { + val separator = DecimalFormatSymbols.getInstance().decimalSeparator val separatorCount = min(1, currency.defaultFractionDigits) val prefix: String = currency.getSymbol(Locale.getDefault()) - val pattern: Pattern = "[0-9]*([.,]){0,$separatorCount}[0-9]{0,${currency.defaultFractionDigits}}".toPattern() + val pattern: Pattern = "[0-9]*($separator){0,$separatorCount}[0-9]{0,${currency.defaultFractionDigits}}".toPattern() override fun filter( source: CharSequence, @@ -198,6 +204,11 @@ data class Boost( val result = dest.subSequence(0, dstart).toString() + source.toString() + dest.subSequence(dend, dest.length) val resultWithoutCurrencyPrefix = result.removePrefix(prefix) + + if (result.length == 1 && !result.isDigitsOnly() && result != separator.toString()) { + return dest.subSequence(dstart, dend) + } + val matcher = pattern.matcher(resultWithoutCurrencyPrefix) if (!matcher.matches()) { 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 abaea2148a..b582722a02 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 @@ -132,6 +132,11 @@ class BoostFragment : DSLSettingsBottomSheetFragment( } } + override fun onDestroyView() { + super.onDestroyView() + processingDonationPaymentDialog.hide() + } + private fun getConfiguration(state: BoostState): DSLConfiguration { if (state.stage == BoostState.Stage.PAYMENT_PIPELINE) { processingDonationPaymentDialog.show() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostViewModel.kt index fcb73d9dcb..ed2e5e3b59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/boost/BoostViewModel.kt @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.util.PlatformCurrencyUtil import org.thoughtcrime.securesms.util.livedata.Store import java.math.BigDecimal +import java.text.DecimalFormatSymbols import java.util.Currency class BoostViewModel( @@ -190,7 +191,7 @@ class BoostViewModel( } fun setCustomAmount(amount: String) { - val bigDecimalAmount = if (amount.isEmpty()) { + val bigDecimalAmount = if (amount.isEmpty() || amount == DecimalFormatSymbols.getInstance().decimalSeparator.toString()) { BigDecimal.ZERO } else { BigDecimal(amount) 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 53e0029bc4..cf784facd6 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 @@ -109,6 +109,11 @@ class SubscribeFragment : DSLSettingsFragment( } } + override fun onDestroyView() { + super.onDestroyView() + processingDonationPaymentDialog.hide() + } + private fun getConfiguration(state: SubscribeState): DSLConfiguration { if (state.hasInProgressSubscriptionTransaction || state.stage == SubscribeState.Stage.PAYMENT_PIPELINE) { processingDonationPaymentDialog.show() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/thanks/ThanksForYourSupportBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/thanks/ThanksForYourSupportBottomSheetDialogFragment.kt index ef772e94f5..1fd7e16827 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/thanks/ThanksForYourSupportBottomSheetDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/thanks/ThanksForYourSupportBottomSheetDialogFragment.kt @@ -108,6 +108,7 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh if (args.isBoost) { presentBoostCopy() + badgeView.visibility = View.INVISIBLE lottie.visible = true lottie.playAnimation() lottie.addAnimatorListener(object : AnimationCompleteListener() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt index 89d581d3fa..3324f87b8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.settings import androidx.annotation.CallSuper import androidx.annotation.Px import androidx.annotation.StringRes +import org.thoughtcrime.securesms.components.settings.models.AsyncSwitch import org.thoughtcrime.securesms.components.settings.models.Button import org.thoughtcrime.securesms.components.settings.models.Space import org.thoughtcrime.securesms.components.settings.models.Text @@ -56,6 +57,17 @@ class DSLConfiguration { children.add(preference) } + fun asyncSwitchPref( + title: DSLSettingsText, + isEnabled: Boolean = true, + isChecked: Boolean, + isProcessing: Boolean, + onClick: () -> Unit + ) { + val preference = AsyncSwitch.Model(title, isEnabled, isChecked, isProcessing, onClick) + children.add(preference) + } + fun switchPref( title: DSLSettingsText, summary: DSLSettingsText? = null, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/models/AsyncSwitch.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/models/AsyncSwitch.kt new file mode 100644 index 0000000000..136024c33b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/models/AsyncSwitch.kt @@ -0,0 +1,54 @@ +package org.thoughtcrime.securesms.components.settings.models + +import android.view.View +import android.widget.ViewSwitcher +import com.google.android.material.switchmaterial.SwitchMaterial +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.settings.DSLSettingsText +import org.thoughtcrime.securesms.components.settings.PreferenceModel +import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder +import org.thoughtcrime.securesms.util.MappingAdapter + +/** + * Switch that will perform a long-running async operation (normally network) that requires a + * progress spinner to replace the switch after a press. + */ +object AsyncSwitch { + + fun register(adapter: MappingAdapter) { + adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(AsyncSwitch::ViewHolder, R.layout.dsl_async_switch_preference_item)) + } + + class Model( + override val title: DSLSettingsText, + override val isEnabled: Boolean, + val isChecked: Boolean, + val isProcessing: Boolean, + val onClick: () -> Unit + ) : PreferenceModel() { + override fun areContentsTheSame(newItem: Model): Boolean { + return super.areContentsTheSame(newItem) && isChecked == newItem.isChecked && isProcessing == newItem.isProcessing + } + } + + class ViewHolder(itemView: View) : PreferenceViewHolder(itemView) { + private val switchWidget: SwitchMaterial = itemView.findViewById(R.id.switch_widget) + private val switcher: ViewSwitcher = itemView.findViewById(R.id.switcher) + + override fun bind(model: Model) { + super.bind(model) + switchWidget.isEnabled = model.isEnabled + switchWidget.isChecked = model.isChecked + itemView.isEnabled = !model.isProcessing + switcher.displayedChild = if (model.isProcessing) 1 else 0 + + itemView.setOnClickListener { + if (!model.isProcessing) { + itemView.isEnabled = false + switcher.displayedChild = 1 + model.onClick() + } + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java index 09ef0bc5a4..ca3ccb2c31 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantView.java @@ -19,6 +19,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.signal.core.util.ThreadUtil; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.badges.BadgeImageView; import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.emoji.EmojiTextView; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; @@ -55,9 +56,11 @@ public class CallParticipantView extends ConstraintLayout { private AppCompatImageView backgroundAvatar; private AvatarImageView avatar; + private BadgeImageView badge; private View rendererFrame; private TextureViewRenderer renderer; private ImageView pipAvatar; + private BadgeImageView pipBadge; private ContactPhoto contactPhoto; private View audioMuted; private View infoOverlay; @@ -92,6 +95,8 @@ public class CallParticipantView extends ConstraintLayout { infoIcon = findViewById(R.id.call_participant_info_icon); infoMessage = findViewById(R.id.call_participant_info_message); infoMoreInfo = findViewById(R.id.call_participant_info_more_info); + badge = findViewById(R.id.call_participant_item_badge); + pipBadge = findViewById(R.id.call_participant_item_pip_badge); avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER); useLargeAvatar(); @@ -120,7 +125,9 @@ public class CallParticipantView extends ConstraintLayout { renderer.attachBroadcastVideoSink(null); audioMuted.setVisibility(View.GONE); avatar.setVisibility(View.GONE); + badge.setVisibility(View.GONE); pipAvatar.setVisibility(View.GONE); + pipBadge.setVisibility(View.GONE); infoOverlay.setVisibility(View.VISIBLE); @@ -157,8 +164,10 @@ public class CallParticipantView extends ConstraintLayout { if (participantChanged || !Objects.equals(contactPhoto, participant.getRecipient().getContactPhoto())) { avatar.setAvatarUsingProfile(participant.getRecipient()); + badge.setBadgeFromRecipient(participant.getRecipient()); AvatarUtil.loadBlurredIconIntoImageView(participant.getRecipient(), backgroundAvatar); setPipAvatar(participant.getRecipient()); + pipBadge.setBadgeFromRecipient(participant.getRecipient()); contactPhoto = participant.getRecipient().getContactPhoto(); } } @@ -193,15 +202,19 @@ public class CallParticipantView extends ConstraintLayout { } avatar.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE); + badge.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE); pipAvatar.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE); + pipBadge.setVisibility(shouldRenderInPip ? View.VISIBLE : View.GONE); } void hideAvatar() { avatar.setAlpha(0f); + badge.setAlpha(0f); } void showAvatar() { avatar.setAlpha(1f); + badge.setAlpha(1f); } void useLargeAvatar() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsListUpdatePopupWindow.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsListUpdatePopupWindow.java index 6cde42600c..46a3381724 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsListUpdatePopupWindow.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/CallParticipantsListUpdatePopupWindow.java @@ -14,6 +14,7 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.badges.BadgeImageView; import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.ViewUtil; @@ -29,6 +30,7 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow { private final ViewGroup parent; private final AvatarImageView avatarImageView; + private final BadgeImageView badgeImageView; private final TextView descriptionTextView; private final Set pendingAdditions = new HashSet<>(); @@ -43,6 +45,7 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow { this.parent = parent; this.avatarImageView = getContentView().findViewById(R.id.avatar); + this.badgeImageView = getContentView().findViewById(R.id.badge); this.descriptionTextView = getContentView().findViewById(R.id.description); setOnDismissListener(this::showPending); @@ -109,6 +112,7 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow { private void setAvatar(@Nullable Recipient recipient) { avatarImageView.setAvatarUsingProfile(recipient); + badgeImageView.setBadgeFromRecipient(recipient); avatarImageView.setVisibility(recipient == null ? View.GONE : View.VISIBLE); } 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 916eff64b0..425e43df12 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -322,6 +322,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode SimpleTask.run(getViewLifecycleOwner().getLifecycle(), Recipient::self, this::initializeProfileIcon); + initializeSettingsTouchTarget(); + if ((!searchToolbar.resolved() || !searchToolbar.get().isVisible()) && list.getAdapter() != defaultAdapter) { list.removeItemDecoration(searchAdapterDecoration); setAdapter(defaultAdapter); @@ -527,7 +529,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode imageView.setBadgeFromRecipient(recipient); AvatarUtil.loadIconIntoImageView(recipient, icon, getResources().getDimensionPixelSize(R.dimen.toolbar_avatar_size)); - icon.setOnClickListener(v -> getNavigator().goToAppSettings()); + } + + private void initializeSettingsTouchTarget() { + View touchArea = requireView().findViewById(R.id.toolbar_settings_touch_area); + touchArea.setOnClickListener(v -> getNavigator().goToAppSettings()); } private void initializeSearchListener() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java index 173252ed71..38c56128a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -226,6 +226,8 @@ public final class ConversationListItem extends ConstraintLayout private void setBadgeFromRecipient(Recipient recipient) { if (!recipient.isSelf()) { badge.setBadgeFromRecipient(recipient); + } else { + badge.setBadge(null); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java index a4950c0d15..e6e5a7a39b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/JobManager.java @@ -462,6 +462,14 @@ public class JobManager implements ConstraintObserver.Notifier { jobManager.enqueueChain(this); } + public void enqueue(@NonNull JobTracker.JobListener listener) { + List lastChain = jobs.get(jobs.size() - 1); + Job lastJobInLastChain = lastChain.get(lastChain.size() - 1); + + jobManager.addListener(lastJobInLastChain.getId(), listener); + enqueue(); + } + private List> getJobListChain() { return jobs; } 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 936a2d0a6c..229741a9b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BoostReceiptRequestResponseJob.java @@ -17,6 +17,7 @@ import org.signal.zkgroup.receipts.ReceiptSerial; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.subscription.SubscriptionNotification; import org.whispersystems.signalservice.internal.ServiceResponse; @@ -56,18 +57,15 @@ public class BoostReceiptRequestResponseJob extends BaseJob { ); } - public static String enqueueChain(StripeApi.PaymentIntent paymentIntent) { + public static JobManager.Chain createJobChain(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 refreshOwnProfileJob.getId(); + return ApplicationDependencies.getJobManager() + .startChain(requestReceiptJob) + .then(redeemReceiptJob) + .then(refreshOwnProfileJob); } private BoostReceiptRequestResponseJob(@NonNull Parameters parameters, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java index aee2c3ad36..b085e18502 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/DonationReceiptRedemptionJob.java @@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.subscription.SubscriptionNotification; import org.whispersystems.signalservice.internal.EmptyResponse; import org.whispersystems.signalservice.internal.ServiceResponse; @@ -63,6 +64,7 @@ public class DonationReceiptRedemptionJob extends BaseJob { @Override public void onFailure() { + SubscriptionNotification.RedemptionFailed.INSTANCE.show(context); } @Override @@ -70,8 +72,8 @@ public class DonationReceiptRedemptionJob extends BaseJob { Data inputData = getInputData(); if (inputData == null) { - Log.w(TAG, "No input data. Failing.", null, true); - throw new IllegalStateException("Expected a presentation object in input data."); + Log.w(TAG, "No input data. Exiting.", null, true); + return; } byte[] presentationBytes = inputData.getStringAsBlob(INPUT_RECEIPT_CREDENTIAL_PRESENTATION); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java index 9c5a05c1dc..ec51a31892 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionKeepAliveJob.java @@ -11,7 +11,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.subscription.Subscriber; import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; -import org.whispersystems.signalservice.api.subscriptions.SubscriberId; import org.whispersystems.signalservice.internal.EmptyResponse; import org.whispersystems.signalservice.internal.ServiceResponse; @@ -97,7 +96,7 @@ public class SubscriptionKeepAliveJob extends BaseJob { if (activeSubscription.getActiveSubscription().getEndOfCurrentPeriod() > SignalStore.donationsValues().getLastEndOfPeriod()) { Log.i(TAG, "Last end of period change. Requesting receipt refresh."); - SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation(); + SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue(); } } 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 d39c32cb7c..95e8ecbcab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SubscriptionReceiptRequestResponseJob.java @@ -16,6 +16,7 @@ import org.signal.zkgroup.receipts.ReceiptSerial; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.subscription.Subscriber; @@ -62,19 +63,16 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob { ); } - public static String enqueueSubscriptionContinuation() { + public static JobManager.Chain createSubscriptionContinuationJobChain() { 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 refreshOwnProfileJob.getId(); + return ApplicationDependencies.getJobManager() + .startChain(requestReceiptJob) + .then(redeemReceiptJob) + .then(refreshOwnProfileJob); } private SubscriptionReceiptRequestResponseJob(@NonNull Parameters parameters, diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java index 538b0c477f..3e711964d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java @@ -226,7 +226,11 @@ public class ManageProfileFragment extends LoggingFragment { } private void presentBadge(@NonNull Optional badge) { - badgeView.setBadge(badge.orNull()); + if (badge.isPresent() && badge.get().getVisible() && !badge.get().isExpired()) { + badgeView.setBadge(badge.orNull()); + } else { + badgeView.setBadge(null); + } } private void presentEvent(@NonNull ManageProfileViewModel.Event event) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/subscription/SubscriptionNotification.kt b/app/src/main/java/org/thoughtcrime/securesms/subscription/SubscriptionNotification.kt index 017a8c68c6..819efd8e99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/subscription/SubscriptionNotification.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/subscription/SubscriptionNotification.kt @@ -38,5 +38,31 @@ sealed class SubscriptionNotification { } } + object RedemptionFailed : SubscriptionNotification() { + override fun show(context: Context) { + val notification = NotificationCompat.Builder(context, NotificationChannels.FAILURES) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(context.getString(R.string.Subscription__redemption_failed)) + .setContentText(context.getString(R.string.Subscription__please_contact_support_for_more_information)) + .addAction( + NotificationCompat.Action.Builder( + null, + context.getString(R.string.Subscription__contact_support), + PendingIntent.getActivity( + context, + 0, + AppSettingsActivity.help(context, HelpFragment.DONATION_INDEX), + if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_ONE_SHOT else 0 + ) + ).build() + ) + .build() + + NotificationManagerCompat + .from(context) + .notify(NotificationIds.SUBSCRIPTION_VERIFY_FAILED, notification) + } + } + abstract fun show(context: Context) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/viewholders/RecipientViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/util/viewholders/RecipientViewHolder.java index 7e76fe37dc..194040cd49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/viewholders/RecipientViewHolder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/viewholders/RecipientViewHolder.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.badges.BadgeImageView; import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.MappingAdapter; @@ -16,6 +17,7 @@ import org.thoughtcrime.securesms.util.MappingViewHolder; public class RecipientViewHolder> extends MappingViewHolder { protected final @Nullable AvatarImageView avatar; + protected final @Nullable BadgeImageView badge; protected final @Nullable TextView name; protected final @Nullable EventListener eventListener; private final boolean quickContactEnabled; @@ -30,6 +32,7 @@ public class RecipientViewHolder> extends Map this.quickContactEnabled = quickContactEnabled; avatar = findViewById(R.id.recipient_view_avatar); + badge = findViewById(R.id.recipient_view_badge); name = findViewById(R.id.recipient_view_name); } @@ -39,6 +42,10 @@ public class RecipientViewHolder> extends Map avatar.setRecipient(model.getRecipient(), quickContactEnabled); } + if (badge != null) { + badge.setBadgeFromRecipient(model.getRecipient()); + } + if (name != null) { name.setText(model.getName(context)); } diff --git a/app/src/main/res/layout/call_participant_item.xml b/app/src/main/res/layout/call_participant_item.xml index e689fed1a9..577dcaf63d 100644 --- a/app/src/main/res/layout/call_participant_item.xml +++ b/app/src/main/res/layout/call_participant_item.xml @@ -28,6 +28,21 @@ app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@tools:sample/avatars" /> + + + + + + + + - + + + app:tint="@color/core_white" /> + app:tint="@color/core_white" /> + app:tint="@color/core_white" /> - + diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml index a52222f51a..d74dd15ce0 100644 --- a/app/src/main/res/layout/conversation_list_fragment.xml +++ b/app/src/main/res/layout/conversation_list_fragment.xml @@ -54,6 +54,15 @@ app:layout_constraintStart_toStartOf="@id/toolbar_icon" app:layout_constraintTop_toTopOf="@id/toolbar_icon" /> + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/mentions_picker_recipient_list_item.xml b/app/src/main/res/layout/mentions_picker_recipient_list_item.xml index 4befd1d758..caaae18088 100644 --- a/app/src/main/res/layout/mentions_picker_recipient_list_item.xml +++ b/app/src/main/res/layout/mentions_picker_recipient_list_item.xml @@ -1,5 +1,5 @@ - + + - + diff --git a/app/src/main/res/layout/payments_home_payment_item.xml b/app/src/main/res/layout/payments_home_payment_item.xml index 82a5aa8805..9a6a1b6c1d 100644 --- a/app/src/main/res/layout/payments_home_payment_item.xml +++ b/app/src/main/res/layout/payments_home_payment_item.xml @@ -19,6 +19,18 @@ app:layout_constraintTop_toTopOf="parent" tools:src="@tools:sample/avatars" /> + + Renew subscription Subscription Verification Failed + Badge Redemption Failed Please contact support for more information. Contact Support Earn a %1$s badge