Updated on 2026-08-14
This commit is contained in:
parent
e17029ce9b
commit
06d1f35002
14 changed files with 877 additions and 56 deletions
|
|
@ -15,6 +15,7 @@ dependencies {
|
|||
/** Project - API */
|
||||
implementation(projects.features.account.api)
|
||||
implementation(projects.features.onramp.api)
|
||||
implementation(projects.features.swap.api)
|
||||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.features.swap.domain.api)
|
||||
implementation(projects.features.swap.domain.models)
|
||||
|
|
@ -30,6 +31,8 @@ dependencies {
|
|||
/** Project - Common */
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.uiMarkets)
|
||||
implementation(projects.common.uiCharts)
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.appCurrency)
|
||||
|
|
@ -51,6 +54,7 @@ dependencies {
|
|||
implementation(projects.domain.appTheme)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.domain.markets)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.ui.onrampTokenList
|
||||
import com.tangem.features.onramp.tokenlist.ui.onrampSwapTokenList
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -25,7 +25,7 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
|
|||
get() = model.state
|
||||
|
||||
override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) {
|
||||
onrampTokenList(state = uiState)
|
||||
onrampSwapTokenList(state = uiState)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ internal class SetNoAvailablePairsTransformerV2(
|
|||
.createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText)
|
||||
|
||||
override fun transform(prevState: TokenListUM): TokenListUM {
|
||||
val totalTokensCount = accountList.values.sumOf { it.size }
|
||||
|
||||
return prevState.copy(
|
||||
availableItems = persistentListOf(),
|
||||
unavailableItems = persistentListOf(),
|
||||
|
|
@ -45,6 +47,7 @@ internal class SetNoAvailablePairsTransformerV2(
|
|||
.toPersistentList(),
|
||||
)
|
||||
}.toPersistentList(),
|
||||
totalTokensCount = totalTokensCount,
|
||||
)
|
||||
} else {
|
||||
TokenListUMData.TokenList(
|
||||
|
|
@ -52,6 +55,7 @@ internal class SetNoAvailablePairsTransformerV2(
|
|||
unavailableConverter.convertList(cryptoCurrencies)
|
||||
.map(TokensListItemUM::Token)
|
||||
}.toPersistentList(),
|
||||
totalTokensCount = totalTokensCount,
|
||||
)
|
||||
},
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,243 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.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.features.onramp.swap.availablepairs.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,
|
||||
)
|
||||
|
||||
val totalCount: StateFlow<Int?> = batchFlow.state
|
||||
.map { it.totalCount }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
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.features.onramp.swap.availablepairs.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.R
|
||||
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.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 convertList(items: List<TokenMarket>): List<MarketsListItemUM> = items.map(::convert)
|
||||
|
||||
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,26 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.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 total: Int,
|
||||
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()
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import com.tangem.domain.core.utils.getOrElse
|
|||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -30,10 +31,13 @@ import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
|
|||
import com.tangem.feature.swap.domain.models.domain.SwapPairLeast
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
|
||||
import com.tangem.features.swap.SwapFeatureToggles
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformerV2
|
||||
import com.tangem.features.onramp.swap.availablepairs.market.SwapMarketsListBatchFlowManager
|
||||
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
|
||||
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.swap.entity.AccountCurrencyUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
|
|
@ -43,6 +47,7 @@ import com.tangem.features.onramp.tokenlist.entity.transformer.*
|
|||
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -64,6 +69,8 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
private val swapFeatureToggles: SwapFeatureToggles,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -76,6 +83,22 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
private val accountListFlow = getAccountListUseCaseFlow()
|
||||
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>(emptyMap())
|
||||
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = getSelectedAppCurrencyUseCase.invokeOrDefault()
|
||||
.stateIn(scope = modelScope, started = SharingStarted.Eagerly, initialValue = AppCurrency.Default)
|
||||
private val searchQueryStateForMarkets = 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 { searchQueryStateForMarkets.value },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
subscribeOnUpdateStateV2()
|
||||
|
|
@ -85,6 +108,11 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
|
||||
initializeSearchBarCallbacks()
|
||||
subscribeOnAvailablePairsUpdates()
|
||||
|
||||
if (swapFeatureToggles.isMarketListFeatureEnabled) {
|
||||
subscribeOnMarketsUpdates()
|
||||
subscribeOnVisibleMarketItems()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTokenListUseCaseFlow(): SharedFlow<List<CryptoCurrencyStatus>> {
|
||||
|
|
@ -421,6 +449,8 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery))
|
||||
|
||||
searchManager.update(newQuery)
|
||||
|
||||
searchQueryStateForMarkets.value = newQuery
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -482,4 +512,59 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
network = currency.network.backendId,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun subscribeOnMarketsUpdates() {
|
||||
combine(
|
||||
flow = searchQueryStateForMarkets
|
||||
.onEach { searchQuery ->
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
searchMarketsListManager.reload(searchQuery)
|
||||
}
|
||||
},
|
||||
flow2 = searchMarketsListManager.uiItems,
|
||||
flow3 = searchMarketsListManager.isInInitialLoadingErrorState,
|
||||
flow4 = searchMarketsListManager.isSearchNotFoundState,
|
||||
flow5 = searchMarketsListManager.totalCount,
|
||||
) { searchQuery, uiItems, isError, isSearchNotFound, total ->
|
||||
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 = {},
|
||||
visibleIdsChanged = { visibleMarketItemIds.value = it },
|
||||
total = total ?: uiItems.size,
|
||||
)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { marketsState ->
|
||||
tokenListUMController.update { it.copy(marketsState = marketsState) }
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnVisibleMarketItems() {
|
||||
modelScope.launch {
|
||||
visibleMarketItemIds.mapNotNull { rawIds ->
|
||||
if (rawIds.isNotEmpty()) {
|
||||
searchMarketsListManager.getBatchKeysByItemIds(rawIds)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.distinctUntilChanged().collectLatest { visibleBatchKeys ->
|
||||
searchMarketsListManager.loadCharts(visibleBatchKeys)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
|
||||
|
||||
private const val LOADING_PLACEHOLDERS_COUNT = 20
|
||||
|
||||
internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) {
|
||||
item(key = "markets_title") {
|
||||
val totalCount = (state as? SwapMarketState.Content)?.total
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append(stringResourceSafe(R.string.markets_common_title))
|
||||
if (totalCount != null) {
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) {
|
||||
append(" $totalCount")
|
||||
}
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.padding(top = TangemTheme.dimens.spacing24, bottom = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
|
||||
when (state) {
|
||||
is SwapMarketState.Loading -> {
|
||||
items(count = LOADING_PLACEHOLDERS_COUNT, key = { "market_placeholder_$it" }) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
is SwapMarketState.LoadingError -> {
|
||||
item(key = "market_loading_error") {
|
||||
LoadingErrorItem(
|
||||
modifier = Modifier.fillParentMaxWidth(),
|
||||
onTryAgain = state.onRetryClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
SwapMarketState.SearchNothingFound -> {
|
||||
item(key = "market_not_found") {
|
||||
SearchNothingFoundText(
|
||||
modifier = Modifier.fillParentMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
is SwapMarketState.Content -> {
|
||||
itemsIndexed(
|
||||
items = state.items,
|
||||
key = { _, item -> item.getComposeKey() },
|
||||
) { index, item ->
|
||||
MarketsListItem(
|
||||
model = item,
|
||||
onClick = { state.onItemClick(item) },
|
||||
modifier = Modifier.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.items.lastIndex,
|
||||
backgroundColor = TangemTheme.colors.background.action,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = onTryAgain)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.padding(TangemTheme.dimens.spacing16),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_search_token_no_result_title),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,24 +8,33 @@ import androidx.compose.foundation.layout.imePadding
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent
|
||||
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
|
||||
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
|
||||
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
|
||||
private const val LOAD_MORE_BUFFER = 25
|
||||
|
||||
/**
|
||||
* Swap select tokens
|
||||
*
|
||||
|
|
@ -49,8 +58,8 @@ internal fun SwapSelectTokens(
|
|||
BackHandler(onBack = state.onBackClick)
|
||||
|
||||
val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection()
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
|
|
@ -60,62 +69,146 @@ internal fun SwapSelectTokens(
|
|||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = 8.dp),
|
||||
) {
|
||||
stickyHeader(key = "header") {
|
||||
AppBarWithBackButton(
|
||||
onBackClick = state.onBackClick,
|
||||
text = stringResourceSafe(id = R.string.common_swap),
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
)
|
||||
}
|
||||
swapSelectTokensContent(
|
||||
state = state,
|
||||
selectFromTokenListComponent = selectFromTokenListComponent,
|
||||
selectFromTokenListState = selectFromTokenListState,
|
||||
selectToTokenListComponent = selectToTokenListComponent,
|
||||
selectToTokenListState = selectToTokenListState,
|
||||
)
|
||||
}
|
||||
|
||||
item(key = "exchange_from", contentType = "exchange_from") {
|
||||
ScrollToTopEffect(state = state, lazyListState = lazyListState)
|
||||
|
||||
MarketsHandlers(
|
||||
state = state,
|
||||
selectToTokenListState = selectToTokenListState,
|
||||
lazyListState = lazyListState,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun LazyListScope.swapSelectTokensContent(
|
||||
state: SwapSelectTokensUM,
|
||||
selectFromTokenListComponent: OnrampTokenListComponent,
|
||||
selectFromTokenListState: TokenListUM,
|
||||
selectToTokenListComponent: AvailableSwapPairsComponent,
|
||||
selectToTokenListState: TokenListUM,
|
||||
) {
|
||||
stickyHeader(key = "header") {
|
||||
AppBarWithBackButton(
|
||||
onBackClick = state.onBackClick,
|
||||
text = stringResourceSafe(id = R.string.common_swap),
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
item(key = "exchange_from", contentType = "exchange_from") {
|
||||
ExchangeCard(
|
||||
state = state.exchangeFrom,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 8.dp, bottom = 12.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
|
||||
if (state.exchangeFrom is ExchangeCardUM.Empty) {
|
||||
with(selectFromTokenListComponent) {
|
||||
content(uiState = selectFromTokenListState, modifier = Modifier)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.exchangeFrom is ExchangeCardUM.Filled) {
|
||||
exchangeToSection(
|
||||
state = state,
|
||||
selectToTokenListComponent = selectToTokenListComponent,
|
||||
selectToTokenListState = selectToTokenListState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun LazyListScope.exchangeToSection(
|
||||
state: SwapSelectTokensUM,
|
||||
selectToTokenListComponent: AvailableSwapPairsComponent,
|
||||
selectToTokenListState: TokenListUM,
|
||||
) {
|
||||
item(key = "exchange_to", contentType = "exchange_to") {
|
||||
if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) {
|
||||
ExchangeCard(
|
||||
state = state.exchangeFrom,
|
||||
state = state.exchangeTo,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 8.dp, bottom = 12.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
|
||||
if (state.exchangeFrom is ExchangeCardUM.Empty) {
|
||||
with(selectFromTokenListComponent) {
|
||||
content(
|
||||
uiState = selectFromTokenListState,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.exchangeFrom is ExchangeCardUM.Filled) {
|
||||
item(key = "exchange_to", contentType = "exchange_to") {
|
||||
if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) {
|
||||
ExchangeCard(
|
||||
state = state.exchangeTo,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.exchangeTo is ExchangeCardUM.Empty) {
|
||||
with(selectToTokenListComponent) {
|
||||
content(
|
||||
uiState = selectToTokenListState,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scroll to top after "from" token selection
|
||||
if (state.exchangeTo is ExchangeCardUM.Empty) {
|
||||
with(selectToTokenListComponent) {
|
||||
content(uiState = selectToTokenListState, modifier = Modifier.padding(horizontal = 16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScrollToTopEffect(state: SwapSelectTokensUM, lazyListState: LazyListState) {
|
||||
LaunchedEffect(state.exchangeFrom !is ExchangeCardUM.Empty) {
|
||||
lazyListState.scrollToItem(index = 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketsHandlers(
|
||||
state: SwapSelectTokensUM,
|
||||
selectToTokenListState: TokenListUM,
|
||||
lazyListState: LazyListState,
|
||||
) {
|
||||
// Markets for "to" token list
|
||||
if (state.exchangeFrom is ExchangeCardUM.Filled && state.exchangeTo is ExchangeCardUM.Empty) {
|
||||
MarketsPaginationHandler(
|
||||
marketsState = selectToTokenListState.marketsState,
|
||||
lazyListState = lazyListState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketsPaginationHandler(marketsState: SwapMarketState?, lazyListState: LazyListState) {
|
||||
(marketsState as? SwapMarketState.Content)?.let { content ->
|
||||
VisibleItemsTracker(lazyListState = lazyListState, marketState = content)
|
||||
|
||||
InfiniteListHandler(
|
||||
listState = lazyListState,
|
||||
buffer = LOAD_MORE_BUFFER,
|
||||
triggerLoadMoreCheckOnItemsCountChange = true,
|
||||
onLoadMore = remember(content) {
|
||||
{
|
||||
content.loadMore()
|
||||
true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapMarketState.Content) {
|
||||
val visibleItems by remember {
|
||||
derivedStateOf {
|
||||
lazyListState.layoutInfo.visibleItemsInfo
|
||||
.mapNotNull { itemInfo ->
|
||||
marketState.items.find { it.getComposeKey() == itemInfo.key }?.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(visibleItems) {
|
||||
marketState.visibleIdsChanged(visibleItems)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.onramp.tokenlist.entity
|
|||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
|
|
@ -12,6 +13,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
* @property availableItems available items (search bar, header, tokens)
|
||||
* @property unavailableItems unavailable items (header, tokens)
|
||||
* @property isBalanceHidden flag that indicates if balance should be hidden
|
||||
* @property marketsState markets list state (null when markets should not be shown)
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
|
@ -22,16 +24,24 @@ internal data class TokenListUM(
|
|||
val tokensListData: TokenListUMData,
|
||||
val isBalanceHidden: Boolean,
|
||||
val warning: NotificationUM? = null,
|
||||
val marketsState: SwapMarketState? = null,
|
||||
)
|
||||
|
||||
internal sealed interface TokenListUMData {
|
||||
|
||||
val totalTokensCount: Int
|
||||
|
||||
data class AccountList(
|
||||
val tokensList: ImmutableList<TokensListItemUM.Portfolio>,
|
||||
override val totalTokensCount: Int,
|
||||
) : TokenListUMData
|
||||
|
||||
data class TokenList(
|
||||
val tokensList: ImmutableList<TokensListItemUM>,
|
||||
override val totalTokensCount: Int,
|
||||
) : TokenListUMData
|
||||
|
||||
data object EmptyList : TokenListUMData
|
||||
data object EmptyList : TokenListUMData {
|
||||
override val totalTokensCount: Int = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,12 @@ internal class SetLoadingAccountTokenListTransformer(
|
|||
private val accountListItemConverter = LoadingAccountTokenItemConverter(appCurrency)
|
||||
|
||||
override fun transform(prevState: TokenListUM): TokenListUM {
|
||||
val totalTokensCount = accountList.sumOf { account ->
|
||||
when (account) {
|
||||
is AccountStatus.CryptoPortfolio -> account.tokenList.flattenCurrencies().size
|
||||
}
|
||||
}
|
||||
|
||||
return prevState.copy(
|
||||
availableItems = persistentListOf(),
|
||||
unavailableItems = persistentListOf(),
|
||||
|
|
@ -27,6 +33,7 @@ internal class SetLoadingAccountTokenListTransformer(
|
|||
tokensList = accountListItemConverter.convertList(
|
||||
accountList.filterIsInstance<AccountStatus.CryptoPortfolio>(),
|
||||
).toPersistentList(),
|
||||
totalTokensCount = totalTokensCount,
|
||||
)
|
||||
} else {
|
||||
TokenListUMData.TokenList(
|
||||
|
|
@ -37,6 +44,7 @@ internal class SetLoadingAccountTokenListTransformer(
|
|||
)
|
||||
}
|
||||
}.toPersistentList(),
|
||||
totalTokensCount = totalTokensCount,
|
||||
)
|
||||
},
|
||||
warning = null,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ internal class SetNothingToFoundStateTransformerV2(
|
|||
text = emptySearchMessageReference,
|
||||
),
|
||||
),
|
||||
totalTokensCount = 0,
|
||||
),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -40,12 +40,15 @@ internal class UpdateAccountTokenListTransformer(
|
|||
.createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText)
|
||||
|
||||
override fun transform(prevState: TokenListUM): TokenListUM {
|
||||
val totalTokensCount = accountList.sumOf { it.currencyList.size }
|
||||
|
||||
return prevState.copy(
|
||||
availableItems = persistentListOf(),
|
||||
unavailableItems = persistentListOf(),
|
||||
tokensListData = if (isAccountsMode) {
|
||||
TokenListUMData.AccountList(
|
||||
tokensList = accountListItemConverter.convertList(accountList).toPersistentList(),
|
||||
totalTokensCount = totalTokensCount,
|
||||
)
|
||||
} else {
|
||||
val tokensList = accountList.flatMap { (_, currencyList) ->
|
||||
|
|
@ -66,6 +69,7 @@ internal class UpdateAccountTokenListTransformer(
|
|||
text = resourceReference(R.string.exchange_tokens_available_tokens_header),
|
||||
),
|
||||
) + tokensList,
|
||||
totalTokensCount = totalTokensCount,
|
||||
)
|
||||
} else {
|
||||
TokenListUMData.EmptyList
|
||||
|
|
|
|||
|
|
@ -3,18 +3,25 @@ package com.tangem.features.onramp.tokenlist.ui
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
|
|
@ -25,25 +32,80 @@ import com.tangem.core.ui.components.tokenlist.TokenListItem
|
|||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||
import com.tangem.core.ui.utils.lazyListItemPosition
|
||||
import com.tangem.features.onramp.swap.availablepairs.ui.swapMarketsListItems
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||
import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Token list
|
||||
* Token list for swap - automatically switches between normal and search mode with markets
|
||||
*
|
||||
* @param state state
|
||||
* @param state state
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal fun LazyListScope.onrampSwapTokenList(state: TokenListUM) {
|
||||
val isSearchMode = state.searchBarUM.query.isNotEmpty() && state.marketsState != null
|
||||
if (isSearchMode) {
|
||||
onrampTokenListWithMarkets(state = state)
|
||||
} else {
|
||||
onrampTokenList(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token list - normal mode (without markets)
|
||||
*
|
||||
* @param state state
|
||||
*/
|
||||
internal fun LazyListScope.onrampTokenList(state: TokenListUM) {
|
||||
val itemModifier = Modifier.padding(horizontal = 16.dp)
|
||||
|
||||
warningOrSearchBar(state = state, itemModifier = itemModifier)
|
||||
|
||||
tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
|
||||
tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
|
||||
tokensListData(state = state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Token list with markets - search mode
|
||||
*
|
||||
* @param state state
|
||||
*/
|
||||
private fun LazyListScope.onrampTokenListWithMarkets(state: TokenListUM) {
|
||||
val itemModifier = Modifier.padding(horizontal = 16.dp)
|
||||
|
||||
warningOrSearchBar(state = state, itemModifier = itemModifier)
|
||||
|
||||
// Check if user has any assets to show
|
||||
val hasAssets = state.availableItems.isNotEmpty() ||
|
||||
state.unavailableItems.isNotEmpty() ||
|
||||
state.tokensListData.totalTokensCount != 0
|
||||
|
||||
if (hasAssets) {
|
||||
assetsTitle(count = state.tokensListData.totalTokensCount)
|
||||
|
||||
tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
|
||||
tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
|
||||
tokensListData(state = state)
|
||||
|
||||
item { SpacerH32() }
|
||||
}
|
||||
|
||||
state.marketsState?.let(::swapMarketsListItems)
|
||||
}
|
||||
|
||||
private fun LazyListScope.warningOrSearchBar(state: TokenListUM, itemModifier: Modifier) {
|
||||
if (state.warning == null) {
|
||||
searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier)
|
||||
} else {
|
||||
|
|
@ -67,11 +129,9 @@ internal fun LazyListScope.onrampTokenList(state: TokenListUM) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
|
||||
tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
|
||||
private fun LazyListScope.tokensListData(state: TokenListUM) {
|
||||
when (val list = state.tokensListData) {
|
||||
is TokenListUMData.AccountList -> list.tokensList.forEach { item ->
|
||||
portfolioTokensList(
|
||||
|
|
@ -99,6 +159,28 @@ private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modi
|
|||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.assetsTitle(count: Int) {
|
||||
item(key = "assets_title") {
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append(stringResourceSafe(R.string.swap_your_assets_title))
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) {
|
||||
append(" $count")
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.tokensList(items: ImmutableList<TokensListItemUM>, isBalanceHidden: Boolean) {
|
||||
itemsIndexed(
|
||||
items = items,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue