Updated on 2026-08-14
This commit is contained in:
parent
f28ffbc0f9
commit
0ced6def88
12 changed files with 524 additions and 8 deletions
|
|
@ -66,5 +66,9 @@
|
|||
{
|
||||
"name": "GASLESS_TRANSACTIONS_ENABLED",
|
||||
"version": "5.33.0"
|
||||
},
|
||||
{
|
||||
"name": "SWAP_MARKET_LIST_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
package com.tangem.features.swap
|
||||
|
||||
interface SwapFeatureToggles
|
||||
interface SwapFeatureToggles {
|
||||
val isMarketListFeatureEnabled: Boolean
|
||||
}
|
||||
|
|
@ -23,6 +23,8 @@ dependencies {
|
|||
implementation(projects.common.ui)
|
||||
implementation(projects.core.decompose) // For Route supertype
|
||||
implementation(projects.core.error)
|
||||
implementation(projects.common.uiMarkets)
|
||||
implementation(projects.common.uiCharts)
|
||||
|
||||
/** Domain modules **/
|
||||
implementation(projects.domain.models)
|
||||
|
|
@ -48,6 +50,7 @@ dependencies {
|
|||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.account.status)
|
||||
implementation(projects.domain.visa)
|
||||
implementation(projects.domain.markets)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.swap.domain)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
package com.tangem.feature.swap
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
|
||||
internal class DefaultSwapFeatureToggles : SwapFeatureToggles
|
||||
internal class DefaultSwapFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : SwapFeatureToggles {
|
||||
override val isMarketListFeatureEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("SWAP_MARKET_LIST_ENABLED")
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ internal class TokensDataConverterV2(
|
|||
|
||||
override fun transform(prevState: SwapStateHolder): SwapStateHolder {
|
||||
val accountList = tokensDataState.accountCurrencyList
|
||||
val currentMarketsState = prevState.selectTokenState?.marketsState
|
||||
return prevState.copy(
|
||||
selectTokenState = SwapSelectTokenStateHolder(
|
||||
availableTokens = persistentListOf(),
|
||||
|
|
@ -57,6 +58,7 @@ internal class TokensDataConverterV2(
|
|||
}.toPersistentList(),
|
||||
)
|
||||
},
|
||||
marketsState = currentMarketsState,
|
||||
onSearchEntered = onSearchEntered,
|
||||
onTokenSelected = onTokenSelected,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.swap.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.feature.swap.DefaultSwapComponent
|
||||
import com.tangem.feature.swap.DefaultSwapFeatureToggles
|
||||
import com.tangem.features.swap.SwapComponent
|
||||
|
|
@ -17,8 +18,8 @@ internal object SwapFeatureModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSwapFeatureToggles(): SwapFeatureToggles {
|
||||
return DefaultSwapFeatureToggles()
|
||||
fun provideSwapFeatureToggles(featureTogglesManager: FeatureTogglesManager): SwapFeatureToggles {
|
||||
return DefaultSwapFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
|||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
|
|
@ -64,6 +65,8 @@ import com.tangem.feature.swap.domain.models.domain.*
|
|||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.UiActions
|
||||
import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import com.tangem.feature.swap.models.states.SwapNotificationUM
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.feature.swap.router.SwapNavScreen
|
||||
|
|
@ -71,6 +74,7 @@ import com.tangem.feature.swap.router.SwapRouter
|
|||
import com.tangem.feature.swap.ui.StateBuilder
|
||||
import com.tangem.feature.swap.utils.formatToUIRepresentation
|
||||
import com.tangem.features.swap.SwapComponent
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
|
||||
import com.tangem.utils.coroutines.*
|
||||
|
|
@ -120,6 +124,8 @@ internal class SwapModel @Inject constructor(
|
|||
private val getTangemPayCurrencyStatusUseCase: GetTangemPayCurrencyStatusUseCase,
|
||||
private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase,
|
||||
private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
|
||||
private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
private val swapFeatureToggles: SwapFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<SwapComponent.Params>()
|
||||
|
|
@ -160,6 +166,7 @@ internal class SwapModel @Inject constructor(
|
|||
?: error("NumberFormat is not DecimalFormat"),
|
||||
)
|
||||
private val amountDebouncer = Debouncer()
|
||||
private val searchDebouncer = Debouncer()
|
||||
private val singleTaskScheduler = SingleTaskScheduler<Map<SwapProvider, SwapState>>()
|
||||
|
||||
private var dataState by mutableStateOf(SwapProcessDataState())
|
||||
|
|
@ -197,6 +204,19 @@ internal class SwapModel @Inject constructor(
|
|||
private var isAmountChangedByUser: Boolean = false
|
||||
private var lastPermissionNotificationTokens: Pair<String, String>? = null
|
||||
|
||||
private val searchQueryState = MutableStateFlow("")
|
||||
private val visibleMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
private val searchMarketsListManager by lazy {
|
||||
SwapMarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
|
||||
currentAppCurrency = Provider { selectedAppCurrencyFlow.value },
|
||||
currentSearchText = Provider { searchQueryState.value },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
val currentScreen: SwapNavScreen
|
||||
get() = swapRouter.currentScreen
|
||||
|
||||
|
|
@ -259,6 +279,61 @@ internal class SwapModel @Inject constructor(
|
|||
uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
||||
if (swapFeatureToggles.isMarketListFeatureEnabled) {
|
||||
combine(
|
||||
flow = searchQueryState
|
||||
.onEach { searchQuery ->
|
||||
searchMarketsListManager.reload(searchQuery)
|
||||
},
|
||||
flow2 = searchMarketsListManager.uiItems,
|
||||
flow3 = searchMarketsListManager.isInInitialLoadingErrorState,
|
||||
flow4 = searchMarketsListManager.isSearchNotFoundState,
|
||||
) { searchQuery, uiItems, isError, isSearchNotFound ->
|
||||
when {
|
||||
searchQuery.isEmpty() -> {
|
||||
visibleMarketItemIds.value = emptyList()
|
||||
null
|
||||
}
|
||||
isError -> SwapMarketState.LoadingError(
|
||||
onRetryClicked = { searchMarketsListManager.reload(searchQuery) },
|
||||
)
|
||||
isSearchNotFound -> SwapMarketState.SearchNothingFound
|
||||
uiItems.isEmpty() -> SwapMarketState.Loading
|
||||
else -> SwapMarketState.Content(
|
||||
items = uiItems,
|
||||
loadMore = { searchMarketsListManager.loadMore() },
|
||||
onItemClick = { item ->
|
||||
// TODO [REDACTED_TASK_KEY] Add currency to swap form market list
|
||||
},
|
||||
visibleIdsChanged = { visibleMarketItemIds.value = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { marketsState ->
|
||||
uiState.selectTokenState?.let { currentSelectState ->
|
||||
uiState = uiState.copy(
|
||||
selectTokenState = currentSelectState.copy(
|
||||
marketsState = marketsState,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
visibleMarketItemIds.mapNotNull { rawIDS ->
|
||||
if (rawIDS.isNotEmpty()) {
|
||||
searchMarketsListManager.getBatchKeysByItemIds(rawIDS)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.distinctUntilChanged().collectLatest { visibleBatchKeys ->
|
||||
searchMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
|
|
@ -987,8 +1062,10 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onSearchEntered(searchQuery: String) {
|
||||
modelScope.launch(dispatchers.io) {
|
||||
val tokenDataState = dataState.tokensDataState ?: return@launch
|
||||
searchDebouncer.debounce(modelScope, DEBOUNCE_SEARCH_DELAY) {
|
||||
searchQueryState.value = searchQuery
|
||||
|
||||
val tokenDataState = dataState.tokensDataState ?: return@debounce
|
||||
val group = if (isOrderReversed) {
|
||||
tokenDataState.fromGroup
|
||||
} else {
|
||||
|
|
@ -1811,6 +1888,7 @@ internal class SwapModel @Inject constructor(
|
|||
const val INITIAL_AMOUNT = ""
|
||||
const val UPDATE_DELAY = 10000L
|
||||
const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
||||
const val DEBOUNCE_SEARCH_DELAY = 500L
|
||||
const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
|
||||
const val CHANGELLY_PROVIDER_ID = "changelly"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ package com.tangem.feature.swap.models
|
|||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.feature.swap.models.market.state.SwapMarketState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal data class SwapSelectTokenStateHolder(
|
||||
val availableTokens: ImmutableList<TokenToSelectState>,
|
||||
val unavailableTokens: ImmutableList<TokenToSelectState>,
|
||||
val marketsState: SwapMarketState? = null,
|
||||
val tokensListData: TokenListUMData,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isAfterSearch: Boolean,
|
||||
|
|
@ -51,4 +53,14 @@ internal sealed interface TokenListUMData {
|
|||
data object EmptyList : TokenListUMData {
|
||||
override val tokensList: ImmutableList<TokensListItemUM> = persistentListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val SwapSelectTokenStateHolder.isNotFoundState: Boolean
|
||||
get() = availableTokens.isEmpty() && unavailableTokens.isEmpty() &&
|
||||
tokensListData.tokensList.isEmpty() && isAfterSearch &&
|
||||
marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading
|
||||
|
||||
internal val SwapSelectTokenStateHolder.isEmptyState: Boolean
|
||||
get() = availableTokens.isEmpty() && unavailableTokens.isEmpty() &&
|
||||
tokensListData.tokensList.isEmpty() && !isAfterSearch &&
|
||||
marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
package com.tangem.feature.swap.models.market
|
||||
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.feature.swap.models.market.converter.SwapMarketsTokenItemConverter
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SwapMarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType,
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val currentSearchText: Provider<String?>,
|
||||
private val modelScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val actionsFlow = MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>()
|
||||
private val updateStateJob = JobHolder()
|
||||
|
||||
private val batchFlow = getMarketsTokenListFlowUseCase(
|
||||
batchingContext = TokenListBatchingContext(
|
||||
actionsFlow = actionsFlow,
|
||||
coroutineScope = modelScope,
|
||||
),
|
||||
batchFlowType = batchFlowType,
|
||||
)
|
||||
|
||||
private val resultBatches = MutableStateFlow(ResultBatches())
|
||||
private val uiBatches = resultBatches.map { it.uiBatches }
|
||||
|
||||
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>>
|
||||
get() = uiBatches
|
||||
.map { batches ->
|
||||
batches.asSequence()
|
||||
.map { it.data }
|
||||
.flatten()
|
||||
.toImmutableList()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = persistentListOf(),
|
||||
)
|
||||
|
||||
val isInInitialLoadingErrorState = batchFlow.state
|
||||
.map { it.status is PaginationStatus.InitialLoadingError }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val isSearchNotFoundState = batchFlow.state
|
||||
.map { batchListState ->
|
||||
currentSearchText().isNullOrEmpty().not() &&
|
||||
batchListState.status is PaginationStatus.EndOfPagination &&
|
||||
batchListState.data.isEmpty()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
init {
|
||||
batchFlow.state
|
||||
.map { it.data }
|
||||
.distinctUntilChanged { a, b ->
|
||||
a.size == b.size &&
|
||||
a.map { it.key } == b.map { it.key } &&
|
||||
a.map { it.data }.flatten() == b.map { it.data }.flatten()
|
||||
}
|
||||
.onEach {
|
||||
coroutineScope {
|
||||
launch {
|
||||
updateState(it)
|
||||
}.saveIn(updateStateJob)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun updateState(newList: List<Batch<Int, List<TokenMarket>>>, forceUpdate: Boolean = false) =
|
||||
withContext(dispatchers.default) {
|
||||
resultBatches.update { resultBatches ->
|
||||
val items = resultBatches.uiBatches
|
||||
val previousList = resultBatches.processedItems
|
||||
|
||||
val converter = SwapMarketsTokenItemConverter(appCurrency = currentAppCurrency())
|
||||
|
||||
if (newList.isEmpty()) {
|
||||
return@update ResultBatches(processedItems = emptyList())
|
||||
}
|
||||
|
||||
val isInitialLoading =
|
||||
forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key
|
||||
|
||||
val outItems = if (isInitialLoading) {
|
||||
newList.map { batch ->
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = converter.convertList(batch.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (previousList.size != newList.size) {
|
||||
val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet())
|
||||
val newBatches = newList.filter { keysToAdd.contains(it.key) }
|
||||
|
||||
items + newBatches.map { batch ->
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = converter.convertList(batch.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items.mapIndexed { batchIndex, batch ->
|
||||
val prevBatch = previousList[batchIndex]
|
||||
val newBatch = newList[batchIndex]
|
||||
if (prevBatch == newBatch) return@mapIndexed batch
|
||||
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = batch.data.mapIndexed { index, marketsListItemUM ->
|
||||
val prevItem = prevBatch.data.getOrNull(index)
|
||||
val newItem = newBatch.data.getOrNull(index)
|
||||
if (prevItem != null && newItem != null) {
|
||||
converter.update(prevItem, marketsListItemUM, newItem)
|
||||
} else {
|
||||
newItem?.let { converter.convert(it) } ?: marketsListItemUM
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentCoroutineContext().ensureActive()
|
||||
|
||||
ResultBatches(
|
||||
uiBatches = outItems,
|
||||
processedItems = newList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun reload(searchText: String? = null) {
|
||||
modelScope.launch {
|
||||
resultBatches.value = ResultBatches()
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TokenMarketListConfig(
|
||||
fiatPriceCurrency = currentAppCurrency().code,
|
||||
searchText = if (currentSearchText() == null) {
|
||||
null
|
||||
} else {
|
||||
searchText ?: currentSearchText()
|
||||
},
|
||||
priceChangeInterval = TokenMarketListConfig.Interval.H24,
|
||||
order = TokenMarketListConfig.Order.ByRating,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMore() {
|
||||
modelScope.launch {
|
||||
actionsFlow.emit(BatchAction.LoadMore())
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCharts(batchKeys: Set<Int>) {
|
||||
if (batchKeys.isEmpty()) return
|
||||
|
||||
modelScope.launch {
|
||||
val currentData = batchFlow.state.value.data
|
||||
val alreadyLoadedChartsBatchKeys = currentData
|
||||
.filter { batch ->
|
||||
val first = batch.data.firstOrNull() ?: return@filter false
|
||||
first.tokenCharts.h24 != null
|
||||
}
|
||||
.map { it.key }
|
||||
.toSet()
|
||||
|
||||
val batchesKeysToLoad = batchKeys.minus(alreadyLoadedChartsBatchKeys)
|
||||
|
||||
if (batchesKeysToLoad.isNotEmpty()) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.UpdateBatches(
|
||||
keys = batchesKeysToLoad,
|
||||
updateRequest = TokenMarketUpdateRequest.UpdateChart(
|
||||
interval = TokenMarketListConfig.Interval.H24,
|
||||
currency = currentAppCurrency().code,
|
||||
),
|
||||
async = true,
|
||||
operationId = batchesKeysToLoad.toString() + "h24",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getBatchKeysByItemIds(ids: List<CryptoCurrency.RawID>): Set<Int> {
|
||||
val currentData = batchFlow.state.value.data
|
||||
|
||||
return currentData
|
||||
.filter { d -> d.data.any { ids.contains(it.id) } }
|
||||
.map { it.key }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
private data class ResultBatches(
|
||||
val uiBatches: List<Batch<Int, List<MarketsListItemUM>>> = emptyList(),
|
||||
val processedItems: List<Batch<Int, List<TokenMarket>>>? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package com.tangem.feature.swap.models.market.converter
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class SwapMarketsTokenItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<TokenMarket, MarketsListItemUM> {
|
||||
|
||||
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = false)
|
||||
|
||||
override fun convert(value: TokenMarket): MarketsListItemUM {
|
||||
return MarketsListItemUM(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
currencySymbol = value.symbol,
|
||||
ratingPosition = value.marketRating?.toString(),
|
||||
marketCap = value.getMarketCap(),
|
||||
iconUrl = value.imageUrlLarge,
|
||||
price = value.getCurrentPrice(),
|
||||
trendPercentText = value.getTrendPercent(),
|
||||
trendType = value.getTrendType(),
|
||||
chartData = value.getChartData(),
|
||||
isUnder100kMarketCap = value.isUnderMarketCapLimit,
|
||||
stakingRate = value.yieldRate?.format { percent() }?.let {
|
||||
resourceReference(R.string.markets_apy_placeholder, wrappedList(it))
|
||||
},
|
||||
updateTimestamp = value.updateTimestamp,
|
||||
)
|
||||
}
|
||||
|
||||
fun update(prev: TokenMarket, prevUI: MarketsListItemUM, new: TokenMarket): MarketsListItemUM {
|
||||
require(prev.id == new.id) {
|
||||
"Ids is not the same during update TokenMarket item: previousItem[${prev.id}] != newItem[${new.id}]"
|
||||
}
|
||||
|
||||
return prevUI.copy(
|
||||
name = new.name,
|
||||
currencySymbol = new.symbol,
|
||||
ratingPosition = new.marketRating?.toString(),
|
||||
marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() },
|
||||
iconUrl = new.imageUrlLarge,
|
||||
price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) {
|
||||
new.getCurrentPrice(
|
||||
prev = prev,
|
||||
)
|
||||
},
|
||||
trendPercentText = ifChanged(
|
||||
prev.tokenQuotesShort,
|
||||
new.tokenQuotesShort,
|
||||
prevUI.trendPercentText,
|
||||
) { new.getTrendPercent() },
|
||||
trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() },
|
||||
chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() },
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <T, R> ifChanged(prev: T, new: T, prevR: R, force: Boolean = false, change: (T) -> R): R {
|
||||
return if (force || prev != new) change(new) else prevR
|
||||
}
|
||||
|
||||
private fun TokenMarket.getMarketCap(): String? {
|
||||
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
|
||||
|
||||
return value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).compact(
|
||||
threeDigitsMethod = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
|
||||
val prevPrice = prev?.tokenQuotesShort?.currentPrice
|
||||
|
||||
val priceText = tokenQuotesShort.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).price()
|
||||
}
|
||||
|
||||
val changeType = if (prevPrice != null) {
|
||||
if (tokenQuotesShort.currentPrice > prevPrice) {
|
||||
PriceChangeType.UP
|
||||
} else {
|
||||
PriceChangeType.DOWN
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return MarketsListItemUM.Price(
|
||||
text = priceText,
|
||||
changeType = changeType,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarket.getChartData(): MarketChartRawData? {
|
||||
val chart = tokenCharts.h24
|
||||
|
||||
return chart?.let { ct ->
|
||||
priceAndTimePointValuesConverter.convert(
|
||||
MarketChartData.Data(
|
||||
y = ct.priceY.toImmutableList(),
|
||||
x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
).sorted(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun TokenMarket.getTrendType(): PriceChangeType {
|
||||
val percent = tokenQuotesShort.h24ChangePercent
|
||||
val scaled = percent?.setScale(4, RoundingMode.HALF_UP)
|
||||
return when {
|
||||
scaled == null -> PriceChangeType.NEUTRAL
|
||||
scaled > BigDecimal.ZERO -> PriceChangeType.UP
|
||||
scaled < BigDecimal.ZERO -> PriceChangeType.DOWN
|
||||
else -> PriceChangeType.NEUTRAL
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getTrendPercent(): String {
|
||||
val percent = tokenQuotesShort.h24ChangePercent
|
||||
return percent.format { percent() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.feature.swap.models.market.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal sealed class SwapMarketState {
|
||||
|
||||
data class Content(
|
||||
val items: ImmutableList<MarketsListItemUM>,
|
||||
val loadMore: () -> Unit,
|
||||
val onItemClick: (MarketsListItemUM) -> Unit,
|
||||
val visibleIdsChanged: (List<CryptoCurrency.RawID>) -> Unit,
|
||||
) : SwapMarketState()
|
||||
|
||||
data object Loading : SwapMarketState()
|
||||
|
||||
data class LoadingError(
|
||||
val onRetryClicked: () -> Unit,
|
||||
) : SwapMarketState()
|
||||
|
||||
data object SearchNothingFound : SwapMarketState()
|
||||
}
|
||||
|
|
@ -617,13 +617,14 @@ internal class StateBuilder(
|
|||
fromToken: CryptoCurrency,
|
||||
tokensDataState: CurrenciesGroup,
|
||||
): SwapStateHolder {
|
||||
val currentMarketsState = uiState.selectTokenState?.marketsState
|
||||
return uiState.copy(
|
||||
selectTokenState = tokensDataConverter.convert(
|
||||
value = CurrenciesGroupWithFromCurrency(
|
||||
fromCurrency = fromToken,
|
||||
group = tokensDataState,
|
||||
),
|
||||
),
|
||||
).copy(marketsState = currentMarketsState),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue