mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-26 12:44:38 +00:00
Add better UX while loading sustainer data and when a load failure happens.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.text.Editable
|
||||
import android.text.Spanned
|
||||
import android.text.TextWatcher
|
||||
@@ -7,7 +10,10 @@ import android.text.method.DigitsKeyListener
|
||||
import android.view.View
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -44,6 +50,45 @@ data class Boost(
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingModel : PreferenceModel<LoadingModel>() {
|
||||
override fun areItemsTheSame(newItem: LoadingModel): Boolean = true
|
||||
}
|
||||
|
||||
class LoadingViewHolder(itemView: View) : MappingViewHolder<LoadingModel>(itemView), DefaultLifecycleObserver {
|
||||
|
||||
private val animator: Animator = AnimatorSet().apply {
|
||||
val fadeTo25Animator = ObjectAnimator.ofFloat(itemView, "alpha", 0.8f, 0.25f).apply {
|
||||
duration = 1000L
|
||||
}
|
||||
|
||||
val fadeTo80Animator = ObjectAnimator.ofFloat(itemView, "alpha", 0.25f, 0.8f).apply {
|
||||
duration = 300L
|
||||
}
|
||||
|
||||
playSequentially(fadeTo25Animator, fadeTo80Animator)
|
||||
doOnEnd { start() }
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun bind(model: LoadingModel) {
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
if (animator.isStarted) {
|
||||
animator.resume()
|
||||
} else {
|
||||
animator.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
animator.pause()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget that allows a user to select from six different amounts, or enter a custom amount.
|
||||
*/
|
||||
@@ -184,6 +229,7 @@ data class Boost(
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(SelectionModel::class.java, MappingAdapter.LayoutFactory({ SelectionViewHolder(it) }, R.layout.boost_preference))
|
||||
adapter.registerFactory(HeadingModel::class.java, MappingAdapter.LayoutFactory({ HeadingViewHolder(it) }, R.layout.boost_preview_preference))
|
||||
adapter.registerFactory(LoadingModel::class.java, MappingAdapter.LayoutFactory({ LoadingViewHolder(it) }, R.layout.boost_loading_preference))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationE
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.Progress
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
@@ -82,6 +83,7 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||
Boost.register(adapter)
|
||||
GooglePayButton.register(adapter)
|
||||
Progress.register(adapter)
|
||||
NetworkFailure.register(adapter)
|
||||
|
||||
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(R.layout.processing_payment_dialog)
|
||||
@@ -163,11 +165,17 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||
)
|
||||
)
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
if (state.stage == BoostState.Stage.INIT) {
|
||||
customPref(
|
||||
Progress.Model(
|
||||
title = DSLSettingsText.from(R.string.load_more_header__loading)
|
||||
)
|
||||
Boost.LoadingModel()
|
||||
)
|
||||
} else if (state.stage == BoostState.Stage.FAILURE) {
|
||||
space(DimensionUnit.DP.toPixels(20f).toInt())
|
||||
customPref(
|
||||
NetworkFailure.Model {
|
||||
viewModel.retry()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
customPref(
|
||||
|
||||
@@ -45,9 +45,8 @@ class BoostViewModel(
|
||||
.internetConnectionObserver()
|
||||
.distinctUntilChanged()
|
||||
.subscribe { isConnected ->
|
||||
if (!disposables.isDisposed && isConnected && store.state.stage == BoostState.Stage.FAILURE) {
|
||||
store.update { it.copy(stage = BoostState.Stage.INIT) }
|
||||
refresh()
|
||||
if (isConnected) {
|
||||
retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +60,13 @@ class BoostViewModel(
|
||||
return store.state.supportedCurrencyCodes
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (!disposables.isDisposed && store.state.stage == BoostState.Stage.FAILURE) {
|
||||
store.update { it.copy(stage = BoostState.Stage.INIT) }
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.models
|
||||
|
||||
import android.view.View
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
/**
|
||||
* NetworkFailure will display a "card" to the user informing them that there
|
||||
* was a failure and give them a button which allows them to retry fetching data.
|
||||
*/
|
||||
object NetworkFailure {
|
||||
|
||||
class Model(
|
||||
val onRetryClick: () -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val retryButton = itemView.findViewById<MaterialButton>(R.id.retry_button)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
retryButton.setOnClickListener { model.onRetryClick() }
|
||||
}
|
||||
}
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.network_failure_pref))
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationP
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.Progress
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
@@ -80,6 +81,7 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
Subscription.register(adapter)
|
||||
GooglePayButton.register(adapter)
|
||||
Progress.register(adapter)
|
||||
NetworkFailure.register(adapter)
|
||||
|
||||
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(R.layout.processing_payment_dialog)
|
||||
@@ -147,12 +149,19 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
|
||||
space(DimensionUnit.DP.toPixels(4f).toInt())
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
if (state.stage == SubscribeState.Stage.INIT) {
|
||||
customPref(
|
||||
Progress.Model(
|
||||
title = DSLSettingsText.from(R.string.load_more_header__loading)
|
||||
)
|
||||
Subscription.LoaderModel()
|
||||
)
|
||||
} else if (state.stage == SubscribeState.Stage.FAILURE) {
|
||||
space(DimensionUnit.DP.toPixels(69f).toInt())
|
||||
customPref(
|
||||
NetworkFailure.Model {
|
||||
viewModel.refresh()
|
||||
}
|
||||
)
|
||||
space(DimensionUnit.DP.toPixels(75f).toInt())
|
||||
} else {
|
||||
state.subscriptions.forEach {
|
||||
val isActive = state.activeSubscription?.activeSubscription?.level == it.level
|
||||
|
||||
@@ -52,9 +52,8 @@ class SubscribeViewModel(
|
||||
.internetConnectionObserver()
|
||||
.distinctUntilChanged()
|
||||
.subscribe { isConnected ->
|
||||
if (!disposables.isDisposed && isConnected && store.state.stage == SubscribeState.Stage.FAILURE) {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.INIT) }
|
||||
refresh()
|
||||
if (isConnected) {
|
||||
retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +71,13 @@ class SubscribeViewModel(
|
||||
return store.state.subscriptions.firstOrNull()?.prices?.map { it.currency.currencyCode }
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (!disposables.isDisposed && store.state.stage == SubscribeState.Stage.FAILURE) {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.INIT) }
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
package org.thoughtcrime.securesms.subscription
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
@@ -30,6 +36,46 @@ data class Subscription(
|
||||
companion object {
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.subscription_preference))
|
||||
adapter.registerFactory(LoaderModel::class.java, MappingAdapter.LayoutFactory({ LoaderViewHolder(it) }, R.layout.subscription_preference_loader))
|
||||
}
|
||||
}
|
||||
|
||||
class LoaderModel : PreferenceModel<LoaderModel>() {
|
||||
override fun areItemsTheSame(newItem: LoaderModel): Boolean = true
|
||||
}
|
||||
|
||||
class LoaderViewHolder(itemView: View) : MappingViewHolder<LoaderModel>(itemView), DefaultLifecycleObserver {
|
||||
|
||||
private val animator: Animator = AnimatorSet().apply {
|
||||
val fadeTo25Animator = ObjectAnimator.ofFloat(itemView, "alpha", 0.8f, 0.25f).apply {
|
||||
duration = 1000L
|
||||
}
|
||||
|
||||
val fadeTo80Animator = ObjectAnimator.ofFloat(itemView, "alpha", 0.25f, 0.8f).apply {
|
||||
duration = 300L
|
||||
}
|
||||
|
||||
playSequentially(fadeTo25Animator, fadeTo80Animator)
|
||||
doOnEnd { start() }
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun bind(model: LoaderModel) {
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
if (animator.isStarted) {
|
||||
animator.resume()
|
||||
} else {
|
||||
animator.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
animator.pause()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user