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

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.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(

Wyświetl plik

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

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.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

Wyświetl plik

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

Wyświetl plik

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

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__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="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 -->