Add better UX while loading sustainer data and when a load failure happens.

fork-5.53.8
Alex Hart 2021-11-10 11:37:10 -04:00 zatwierdzone przez GitHub
rodzic 1893896254
commit 320bf45518
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
14 zmienionych plików z 350 dodań i 12 usunięć

Wyświetl plik

@ -1,5 +1,8 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.boost 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.Editable
import android.text.Spanned import android.text.Spanned
import android.text.TextWatcher import android.text.TextWatcher
@ -7,7 +10,10 @@ import android.text.method.DigitsKeyListener
import android.view.View import android.view.View
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatEditText
import androidx.core.animation.doOnEnd
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import org.signal.core.util.money.FiatMoney import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R 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. * 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) { fun register(adapter: MappingAdapter) {
adapter.registerFactory(SelectionModel::class.java, MappingAdapter.LayoutFactory({ SelectionViewHolder(it) }, R.layout.boost_preference)) 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(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))
} }
} }
} }

Wyświetl plik

@ -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.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection 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.GooglePayButton
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.Progress import org.thoughtcrime.securesms.components.settings.models.Progress
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
@ -82,6 +83,7 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
Boost.register(adapter) Boost.register(adapter)
GooglePayButton.register(adapter) GooglePayButton.register(adapter)
Progress.register(adapter) Progress.register(adapter)
NetworkFailure.register(adapter)
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext()) processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.processing_payment_dialog) .setView(R.layout.processing_payment_dialog)
@ -163,11 +165,17 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
) )
) )
@Suppress("CascadeIf")
if (state.stage == BoostState.Stage.INIT) { if (state.stage == BoostState.Stage.INIT) {
customPref( customPref(
Progress.Model( Boost.LoadingModel()
title = DSLSettingsText.from(R.string.load_more_header__loading) )
) } else if (state.stage == BoostState.Stage.FAILURE) {
space(DimensionUnit.DP.toPixels(20f).toInt())
customPref(
NetworkFailure.Model {
viewModel.retry()
}
) )
} else { } else {
customPref( customPref(

Wyświetl plik

@ -45,9 +45,8 @@ class BoostViewModel(
.internetConnectionObserver() .internetConnectionObserver()
.distinctUntilChanged() .distinctUntilChanged()
.subscribe { isConnected -> .subscribe { isConnected ->
if (!disposables.isDisposed && isConnected && store.state.stage == BoostState.Stage.FAILURE) { if (isConnected) {
store.update { it.copy(stage = BoostState.Stage.INIT) } retry()
refresh()
} }
} }
} }
@ -61,6 +60,13 @@ class BoostViewModel(
return store.state.supportedCurrencyCodes 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() { fun refresh() {
disposables.clear() disposables.clear()

Wyświetl plik

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

Wyświetl plik

@ -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.SubscriptionsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection 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.GooglePayButton
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.Progress import org.thoughtcrime.securesms.components.settings.models.Progress
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
@ -80,6 +81,7 @@ class SubscribeFragment : DSLSettingsFragment(
Subscription.register(adapter) Subscription.register(adapter)
GooglePayButton.register(adapter) GooglePayButton.register(adapter)
Progress.register(adapter) Progress.register(adapter)
NetworkFailure.register(adapter)
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext()) processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.processing_payment_dialog) .setView(R.layout.processing_payment_dialog)
@ -147,12 +149,19 @@ class SubscribeFragment : DSLSettingsFragment(
space(DimensionUnit.DP.toPixels(4f).toInt()) space(DimensionUnit.DP.toPixels(4f).toInt())
@Suppress("CascadeIf")
if (state.stage == SubscribeState.Stage.INIT) { if (state.stage == SubscribeState.Stage.INIT) {
customPref( customPref(
Progress.Model( Subscription.LoaderModel()
title = DSLSettingsText.from(R.string.load_more_header__loading)
)
) )
} 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 { } else {
state.subscriptions.forEach { state.subscriptions.forEach {
val isActive = state.activeSubscription?.activeSubscription?.level == it.level val isActive = state.activeSubscription?.activeSubscription?.level == it.level

Wyświetl plik

@ -52,9 +52,8 @@ class SubscribeViewModel(
.internetConnectionObserver() .internetConnectionObserver()
.distinctUntilChanged() .distinctUntilChanged()
.subscribe { isConnected -> .subscribe { isConnected ->
if (!disposables.isDisposed && isConnected && store.state.stage == SubscribeState.Stage.FAILURE) { if (isConnected) {
store.update { it.copy(stage = SubscribeState.Stage.INIT) } retry()
refresh()
} }
} }
} }
@ -72,6 +71,13 @@ class SubscribeViewModel(
return store.state.subscriptions.firstOrNull()?.prices?.map { it.currency.currencyCode } 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() { fun refresh() {
disposables.clear() disposables.clear()

Wyświetl plik

@ -1,8 +1,14 @@
package org.thoughtcrime.securesms.subscription package org.thoughtcrime.securesms.subscription
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView 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.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView import org.thoughtcrime.securesms.badges.BadgeImageView
@ -30,6 +36,46 @@ data class Subscription(
companion object { companion object {
fun register(adapter: MappingAdapter) { fun register(adapter: MappingAdapter) {
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.subscription_preference)) 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()
} }
} }

Wyświetl plik

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/signal_button_secondary_stroke" />
<corners android:radius="38dp" />
</shape>

Wyświetl plik

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/signal_button_secondary_stroke" />
<corners android:radius="18dp" />
</shape>

Wyświetl plik

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/signal_button_secondary_stroke" />
<corners android:radius="10dp" />
</shape>

Wyświetl plik

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginTop="20dp"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:alpha="0.8">
<View
android:id="@+id/boost_1"
android:layout_width="0dp"
android:layout_height="48sp"
android:background="@drawable/boost_loading_preference_background"
app:layout_constraintEnd_toStartOf="@id/boost_2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/boost_2"
android:layout_width="0dp"
android:layout_height="48sp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:background="@drawable/boost_loading_preference_background"
app:layout_constraintEnd_toStartOf="@id/boost_3"
app:layout_constraintStart_toEndOf="@id/boost_1"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/boost_3"
android:layout_width="0dp"
android:layout_height="48sp"
android:background="@drawable/boost_loading_preference_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/boost_2"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/boost_4"
android:layout_width="0dp"
android:layout_height="48sp"
android:layout_marginTop="12dp"
android:background="@drawable/boost_loading_preference_background"
app:layout_constraintEnd_toStartOf="@id/boost_5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/boost_1" />
<View
android:id="@+id/boost_5"
android:layout_width="0dp"
android:layout_height="48sp"
android:layout_marginStart="10dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="10dp"
android:background="@drawable/boost_loading_preference_background"
app:layout_constraintEnd_toStartOf="@id/boost_6"
app:layout_constraintStart_toEndOf="@id/boost_4"
app:layout_constraintTop_toBottomOf="@id/boost_2" />
<View
android:id="@+id/boost_6"
android:layout_width="0dp"
android:layout_height="48sp"
android:layout_marginTop="12dp"
android:background="@drawable/boost_loading_preference_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/boost_5"
app:layout_constraintTop_toBottomOf="@id/boost_3" />
<View
android:id="@+id/boost_custom"
android:layout_width="0dp"
android:layout_height="48sp"
android:layout_marginTop="12dp"
android:background="@drawable/boost_loading_preference_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/boost_4" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginEnd="@dimen/dsl_settings_gutter">
<View
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:alpha="0.25"
android:background="@drawable/network_failure_pref_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/error_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="52dp"
android:layout_marginTop="38dp"
android:layout_marginEnd="52dp"
android:text="@string/NetworkFailure__network_error_check_your_connection_and_try_again"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Signal.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/retry_button"
style="@style/Signal.Widget.Button.Large.Secondary.NoOutline"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="42dp"
android:text="@string/NetworkFailure__retry"
app:backgroundTint="@color/white"
app:cornerRadius="38dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/error_text" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="96dp"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginTop="12dp"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:background="@drawable/subscription_loading_preference_background" />
<View
android:layout_width="match_parent"
android:layout_height="96dp"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginTop="12dp"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:background="@drawable/subscription_loading_preference_background" />
<View
android:layout_width="match_parent"
android:layout_height="96dp"
android:layout_marginStart="@dimen/dsl_settings_gutter"
android:layout_marginTop="12dp"
android:layout_marginEnd="@dimen/dsl_settings_gutter"
android:background="@drawable/subscription_loading_preference_background" />
</LinearLayout>

Wyświetl plik

@ -4015,6 +4015,8 @@
<string name="DonationsErrors__failed_to_cancel_subscription">Failed to cancel subscription</string> <string name="DonationsErrors__failed_to_cancel_subscription">Failed to cancel subscription</string>
<string name="DonationsErrors__subscription_cancellation_requires_an_internet_connection">Subscription cancellation requires an internet connection.</string> <string name="DonationsErrors__subscription_cancellation_requires_an_internet_connection">Subscription cancellation requires an internet connection.</string>
<string name="ViewBadgeBottomSheetDialogFragment__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">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.</string> <string name="ViewBadgeBottomSheetDialogFragment__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">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.</string>
<string name="NetworkFailure__network_error_check_your_connection_and_try_again">Network error. Check your connection and try again.</string>
<string name="NetworkFailure__retry">Retry</string>
<!-- EOF --> <!-- EOF -->