Fix a bunch UX bugs for donor badges.

This commit is contained in:
Alex Hart
2021-11-11 13:46:38 -04:00
committed by GitHub
parent 5047fc54f2
commit ca24682366
37 changed files with 450 additions and 47 deletions

View File

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

View File

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

View File

@@ -415,6 +415,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
private fun enqueueSubscriptionRedemption() {
SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation()
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -108,6 +108,7 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh
if (args.isBoost) {
presentBoostCopy()
badgeView.visibility = View.INVISIBLE
lottie.visible = true
lottie.playAnimation()
lottie.addAnimatorListener(object : AnimationCompleteListener() {

View File

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

View File

@@ -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<Model>() {
override fun areContentsTheSame(newItem: Model): Boolean {
return super.areContentsTheSame(newItem) && isChecked == newItem.isChecked && isProcessing == newItem.isProcessing
}
}
class ViewHolder(itemView: View) : PreferenceViewHolder<Model>(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()
}
}
}
}
}

View File

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

View File

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