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 d7c237dc2..98417f3cb 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 @@ -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() { + override fun areItemsTheSame(newItem: LoadingModel): Boolean = true + } + + class LoadingViewHolder(itemView: View) : MappingViewHolder(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)) } } } 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 9e3c6d00d..7fa882fd8 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 @@ -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( 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 76167b99a..fcb73d9dc 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 @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/models/NetworkFailure.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/models/NetworkFailure.kt new file mode 100644 index 000000000..6f5fff089 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/models/NetworkFailure.kt @@ -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() { + override fun areItemsTheSame(newItem: Model): Boolean = true + } + + class ViewHolder(itemView: View) : MappingViewHolder(itemView) { + + private val retryButton = itemView.findViewById(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)) + } +} 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 c9aa695dc..53e0029bc 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 @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt index dfedf6c42..efb628511 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/subscribe/SubscribeViewModel.kt @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/subscription/Subscription.kt b/app/src/main/java/org/thoughtcrime/securesms/subscription/Subscription.kt index 77068d093..579bf15ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/subscription/Subscription.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/subscription/Subscription.kt @@ -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() { + override fun areItemsTheSame(newItem: LoaderModel): Boolean = true + } + + class LoaderViewHolder(itemView: View) : MappingViewHolder(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() } } diff --git a/app/src/main/res/drawable/boost_loading_preference_background.xml b/app/src/main/res/drawable/boost_loading_preference_background.xml new file mode 100644 index 000000000..d53d4797e --- /dev/null +++ b/app/src/main/res/drawable/boost_loading_preference_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/network_failure_pref_background.xml b/app/src/main/res/drawable/network_failure_pref_background.xml new file mode 100644 index 000000000..9eb47cdd0 --- /dev/null +++ b/app/src/main/res/drawable/network_failure_pref_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/subscription_loading_preference_background.xml b/app/src/main/res/drawable/subscription_loading_preference_background.xml new file mode 100644 index 000000000..e7801e417 --- /dev/null +++ b/app/src/main/res/drawable/subscription_loading_preference_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/boost_loading_preference.xml b/app/src/main/res/layout/boost_loading_preference.xml new file mode 100644 index 000000000..dac3bfb1c --- /dev/null +++ b/app/src/main/res/layout/boost_loading_preference.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/network_failure_pref.xml b/app/src/main/res/layout/network_failure_pref.xml new file mode 100644 index 000000000..bef505fef --- /dev/null +++ b/app/src/main/res/layout/network_failure_pref.xml @@ -0,0 +1,49 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/subscription_preference_loader.xml b/app/src/main/res/layout/subscription_preference_loader.xml new file mode 100644 index 000000000..5b3dad948 --- /dev/null +++ b/app/src/main/res/layout/subscription_preference_loader.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fcc86c6e..95a7e6b95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4015,6 +4015,8 @@ Failed to cancel subscription Subscription cancellation requires an internet connection. Your device doesn\'t support Google Pay, so you can\'t subscribe to earn a badge. You can still support Signal by making a donation on our website. + Network error. Check your connection and try again. + Retry