mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Fix a bunch UX bugs for donor badges.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -415,6 +415,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
|
||||
private fun enqueueSubscriptionRedemption() {
|
||||
SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation()
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -108,6 +108,7 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh
|
||||
|
||||
if (args.isBoost) {
|
||||
presentBoostCopy()
|
||||
badgeView.visibility = View.INVISIBLE
|
||||
lottie.visible = true
|
||||
lottie.playAnimation()
|
||||
lottie.addAnimatorListener(object : AnimationCompleteListener() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user