kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add better UX while loading sustainer data and when a load failure happens.
rodzic
1893896254
commit
320bf45518
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 -->
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue