diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index 2175fa9e65..cb8a7cd818 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -82,7 +82,9 @@ internal class FeeSelectorModel @Inject constructor( } private fun loadFee() { - uiState.update(FeeSelectorLoadingTransformer) + if (uiState.value !is FeeSelectorUM.Content) { + uiState.update(FeeSelectorLoadingTransformer) + } modelScope.launch { params.onLoadFee() .fold( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt index d35a67837b..6aee042366 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -3,12 +3,14 @@ package com.tangem.features.send.v2.feeselector.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.* import androidx.compose.ui.text.style.TextAlign @@ -119,7 +121,9 @@ private fun FeeSelectorStaticPart(onReadMoreClick: () -> Unit, modifier: Modifie .size(TangemTheme.dimens.size16), content = { contentModifier -> Icon( - modifier = contentModifier.size(TangemTheme.dimens.size16), + modifier = contentModifier + .size(TangemTheme.dimens.size16) + .clip(CircleShape), painter = painterResource(id = R.drawable.ic_token_info_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt index f0f3fc46a4..dbbf511732 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt @@ -9,6 +9,7 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss +import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel @@ -43,6 +44,10 @@ internal class SwapAmountBlockComponent( ) init { + lifecycle.subscribe( + onStart = model::onStart, + onStop = model::onStop, + ) model.uiState.onEach { onResult(it) }.launchIn(componentScope) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt index 18e1ded3b1..cb025a05e8 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.common.ui.navigationButtons.NavigationModelCallback import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel @@ -23,6 +24,13 @@ internal class SwapAmountComponent @AssistedInject constructor( private val model: SwapAmountModel = getOrCreateModel(params = params) + init { + lifecycle.subscribe( + onStart = model::onStart, + onStop = model::onStop, + ) + } + fun updateState(amountUM: SwapAmountUM) = model.updateState(amountUM) @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 5c95d1518e..dfc08a58f3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -43,6 +43,9 @@ import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderCompone import com.tangem.features.swap.v2.impl.common.SwapAlertFactory import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.Debouncer +import com.tangem.utils.coroutines.PeriodicTask +import com.tangem.utils.coroutines.SingleTaskScheduler import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.update import kotlinx.coroutines.async @@ -91,6 +94,9 @@ internal class SwapAmountModel @Inject constructor( val uiState: StateFlow field = MutableStateFlow(params.amountUM) + private val amountDebouncer = Debouncer() + private val quoteTaskScheduler = SingleTaskScheduler() + init { modelScope.launch { appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } @@ -105,6 +111,19 @@ internal class SwapAmountModel @Inject constructor( subscribeOnReloadQuotesTriggerUpdates() } + fun onStart() { + startLoadingQuotesTask(isSilentReload = false) + } + + fun onStop() { + quoteTaskScheduler.cancelTask() + } + + override fun onDestroy() { + quoteTaskScheduler.cancelTask() + super.onDestroy() + } + fun updateState(amountUM: SwapAmountUM) { uiState.update { amountUM } } @@ -149,7 +168,14 @@ internal class SwapAmountModel @Inject constructor( value = value, ), ) - loadQuotes() + amountDebouncer.debounce( + coroutineScope = modelScope, + waitMs = DEBOUNCE_AMOUNT_DELAY, + forceUpdate = true, + destinationFunction = { + startLoadingQuotesTask(isSilentReload = false) + }, + ) } override fun onAmountPasteTriggerDismiss() { @@ -165,7 +191,7 @@ internal class SwapAmountModel @Inject constructor( secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, ), ) - loadQuotes() + startLoadingQuotesTask(isSilentReload = false) } override fun onCurrencyChangeClick(isFiat: Boolean) { @@ -287,7 +313,7 @@ internal class SwapAmountModel @Inject constructor( ), ) feeSelectorReloadTrigger.triggerLoadingState() - loadQuotes(reloadFees = true) + startLoadingQuotesTask(isSilentReload = false) } .launchIn(modelScope) } @@ -303,7 +329,7 @@ internal class SwapAmountModel @Inject constructor( ), ) feeSelectorReloadTrigger.triggerLoadingState() - loadQuotes(reloadFees = true) + startLoadingQuotesTask(isSilentReload = false) } .launchIn(modelScope) } @@ -316,7 +342,7 @@ internal class SwapAmountModel @Inject constructor( private fun subscribeOnReloadQuotesTriggerUpdates() { swapAmountUpdateListener.reloadQuotesTriggerFlow - .onEach { loadQuotes(reloadFees = true) } + .onEach { startLoadingQuotesTask(isSilentReload = true) } .launchIn(modelScope) } @@ -368,7 +394,7 @@ internal class SwapAmountModel @Inject constructor( isBalanceHidden = params.isBalanceHidingFlow.value, ), ) - loadQuotes() + startLoadingQuotesTask(isSilentReload = false) } else { Timber.e( """ @@ -408,7 +434,7 @@ internal class SwapAmountModel @Inject constructor( } } - private fun loadQuotes(reloadFees: Boolean = false) { + private fun loadQuotes(isSilentReload: Boolean) { val state = uiState.value as? SwapAmountUM.Content ?: return if (state.secondaryCryptoCurrencyStatus == null) return @@ -474,15 +500,35 @@ internal class SwapAmountModel @Inject constructor( quotes = quotes, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + isSilentReload = isSilentReload, ), ) - if (reloadFees) { - feeSelectorReloadTrigger.triggerUpdate() - } + feeSelectorReloadTrigger.triggerUpdate() } } + private fun startLoadingQuotesTask(isSilentReload: Boolean) { + quoteTaskScheduler.cancelTask() + loadQuotes(isSilentReload = isSilentReload) + quoteTaskScheduler.scheduleTask( + scope = modelScope, + task = loadQuotesTask(), + ) + } + + private fun loadQuotesTask(): PeriodicTask { + return PeriodicTask( + delay = QUOTES_UPDATE_DELAY, + isDelayFirst = true, + task = { + runCatching { loadQuotes(isSilentReload = true) } + }, + onSuccess = { /* no-op */ }, + onError = { /* no-op */ }, + ) + } + private suspend fun checkAllowance(state: SwapAmountUM.Content, quote: SwapQuoteModel): Boolean { val allowanceContract = quote.allowanceContract val allowance = if (allowanceContract != null) { @@ -552,4 +598,9 @@ internal class SwapAmountModel @Inject constructor( ) }.launchIn(modelScope) } + + private companion object { + const val DEBOUNCE_AMOUNT_DELAY = 1000L + const val QUOTES_UPDATE_DELAY = 10000L + } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index 05064354de..e6b6c96434 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -19,15 +19,21 @@ internal class SwapAmountSetQuotesTransformer( private val quotes: List, private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, + private val isSilentReload: Boolean, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty + val selectedQuote = if (isSilentReload) { + prevState.selectedQuote + } else { + bestQuote + } val selectQuoteTransformer = SwapAmountSelectQuoteTransformer( - quoteUM = bestQuote, + quoteUM = selectedQuote, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, )