Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-25 17:49:13 +05:00
parent 6416c6f4d1
commit e67a56ba1d
8 changed files with 140 additions and 30 deletions

View file

@ -67,7 +67,10 @@ fun TangemTab(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
)
.padding(all = TangemTheme.dimens2.x3),
.padding(
vertical = TangemTheme.dimens2.x2,
horizontal = TangemTheme.dimens2.x3,
),
) {
Text(
text = text.resolveReference(),

View file

@ -28,7 +28,7 @@ internal class MarketsListBatchFlowManager @AssistedInject constructor(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val dispatchers: CoroutineDispatcherProvider,
@Assisted private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType,
@Assisted private val order: TokenMarketListConfig.Order,
@Assisted private val currentOrder: Provider<TokenMarketListConfig.Order>,
@Assisted private val currentSearchText: Provider<String?>,
@Assisted private val modelScope: CoroutineScope,
) {
@ -192,7 +192,7 @@ internal class MarketsListBatchFlowManager @AssistedInject constructor(
searchText ?: currentSearchText()
},
priceChangeInterval = TokenMarketListConfig.Interval.H24,
order = order,
order = currentOrder(),
shouldNetworks = true,
),
),
@ -262,7 +262,7 @@ internal class MarketsListBatchFlowManager @AssistedInject constructor(
interface Factory {
fun create(
batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType,
order: TokenMarketListConfig.Order,
currentOrder: Provider<TokenMarketListConfig.Order>,
currentSearchText: Provider<String?>,
modelScope: CoroutineScope,
): MarketsListBatchFlowManager

View file

@ -0,0 +1,49 @@
package com.tangem.features.commonfeatures.impl.choosetoken.market.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.markets.TokenMarketListConfig
import kotlinx.collections.immutable.ImmutableList
/**
* Category of the "Market Pulse" block on the Choose asset screen.
*
* Each category maps to a [TokenMarketListConfig.Order] used to fetch the markets list.
*/
internal enum class SwapMarketCategory(
val title: TextReference,
val order: TokenMarketListConfig.Order,
) {
Trending(
title = resourceReference(R.string.markets_sort_by_trending_title),
order = TokenMarketListConfig.Order.Trending,
),
ExperiencedBuyers(
title = resourceReference(R.string.markets_sort_by_experienced_buyers_title),
order = TokenMarketListConfig.Order.Buyers,
),
TopGainers(
title = resourceReference(R.string.markets_sort_by_top_gainers_title),
order = TokenMarketListConfig.Order.TopGainers,
),
TopLosers(
title = resourceReference(R.string.markets_sort_by_top_losers_title),
order = TokenMarketListConfig.Order.TopLosers,
),
}
/**
* UI model for the selectable category tabs of the "Market Pulse" block.
*
* @property items all available categories in display order.
* @property selected currently selected category.
* @property onCategoryClick invoked when a category tab is tapped.
*/
@Immutable
internal data class SwapMarketCategoriesUM(
val items: ImmutableList<SwapMarketCategory>,
val selected: SwapMarketCategory,
val onCategoryClick: (SwapMarketCategory) -> Unit,
)

View file

@ -13,6 +13,8 @@ internal sealed class SwapMarketState {
abstract val marketsTitle: TextReference
abstract val shouldAssetsCount: Boolean
open val categories: SwapMarketCategoriesUM? = null
data class Content(
val items: ImmutableList<MarketsListItemUM>,
val total: Int,
@ -21,17 +23,20 @@ internal sealed class SwapMarketState {
val visibleIdsChanged: (List<CryptoCurrency.RawID>) -> Unit,
override val marketsTitle: TextReference,
override val shouldAssetsCount: Boolean,
override val categories: SwapMarketCategoriesUM? = null,
) : SwapMarketState()
data class Loading(
override val marketsTitle: TextReference,
override val shouldAssetsCount: Boolean,
override val categories: SwapMarketCategoriesUM? = null,
) : SwapMarketState()
data class LoadingError(
val onRetryClicked: () -> Unit,
override val marketsTitle: TextReference,
override val shouldAssetsCount: Boolean,
override val categories: SwapMarketCategoriesUM? = null,
) : SwapMarketState()
data object SearchNothingFound : SwapMarketState() {
@ -40,11 +45,6 @@ internal sealed class SwapMarketState {
}
companion object {
val DefaultLoading
get() = Loading(
marketsTitle = TextReference.Res(R.string.feed_trending_now),
shouldAssetsCount = false,
)
val SearchLoading
get() = Loading(
marketsTitle = TextReference.Res(R.string.markets_common_title),

View file

@ -22,6 +22,8 @@ import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInter
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
import com.tangem.features.commonfeatures.impl.choosetoken.AddToPortfolioRoute
import com.tangem.features.commonfeatures.impl.choosetoken.market.MarketsListBatchFlowManager
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketCategoriesUM
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketCategory
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
@ -49,6 +51,8 @@ internal class MarketBlockDelegate @AssistedInject constructor(
private val visibleMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
private val visibleDefaultMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
private val selectedCategoryFlow = MutableStateFlow(SwapMarketCategory.Trending)
val addToPortfolioSlot: SlotNavigation<AddToPortfolioRoute> = SlotNavigation()
val addToPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create(
scope = modelScope,
@ -91,7 +95,7 @@ internal class MarketBlockDelegate @AssistedInject constructor(
private val defaultMarketsListManager by lazy {
marketsListBatchFlowManagerFactory.create(
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main,
order = TokenMarketListConfig.Order.Trending,
currentOrder = Provider { selectedCategoryFlow.value.order },
currentSearchText = Provider { null },
modelScope = modelScope,
)
@ -100,7 +104,7 @@ internal class MarketBlockDelegate @AssistedInject constructor(
private val searchMarketsListManager by lazy {
marketsListBatchFlowManagerFactory.create(
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
order = TokenMarketListConfig.Order.ByRating,
currentOrder = Provider { TokenMarketListConfig.Order.ByRating },
currentSearchText = Provider { searchQueryState.value.value },
modelScope = modelScope,
)
@ -148,19 +152,26 @@ internal class MarketBlockDelegate @AssistedInject constructor(
}
private fun createDefaultMarketsFlow(): Flow<SwapMarketState> {
val marketsTitle = TextReference.Res(R.string.feed_trending_now)
val marketsTitle = TextReference.Res(R.string.markets_pulse_common_title)
return combine(
defaultMarketsListManager.uiItems,
defaultMarketsListManager.isInInitialLoadingErrorState,
defaultMarketsListManager.totalCount,
) { uiItems, isError, total ->
flow = defaultMarketsListManager.uiItems,
flow2 = defaultMarketsListManager.isInInitialLoadingErrorState,
flow3 = defaultMarketsListManager.totalCount,
flow4 = selectedCategoryFlow,
) { uiItems, isError, total, selectedCategory ->
val categories = buildCategoriesUM(selectedCategory)
when {
isError -> SwapMarketState.LoadingError(
onRetryClicked = { defaultMarketsListManager.reload() },
marketsTitle = marketsTitle,
shouldAssetsCount = false,
categories = categories,
)
uiItems.isEmpty() -> SwapMarketState.Loading(
marketsTitle = marketsTitle,
shouldAssetsCount = false,
categories = categories,
)
uiItems.isEmpty() -> SwapMarketState.DefaultLoading
else -> SwapMarketState.Content(
items = uiItems,
loadMore = { defaultMarketsListManager.loadMore() },
@ -169,11 +180,24 @@ internal class MarketBlockDelegate @AssistedInject constructor(
total = total ?: uiItems.size,
marketsTitle = marketsTitle,
shouldAssetsCount = false,
categories = categories,
)
}
}
}
private fun buildCategoriesUM(selected: SwapMarketCategory): SwapMarketCategoriesUM = SwapMarketCategoriesUM(
items = SwapMarketCategory.entries.toImmutableList(),
selected = selected,
onCategoryClick = ::onCategorySelected,
)
private fun onCategorySelected(category: SwapMarketCategory) {
if (selectedCategoryFlow.value == category) return
selectedCategoryFlow.value = category
defaultMarketsListManager.reload()
}
private fun createSearchMarketsFlow(): Flow<SwapMarketState> {
val marketsTitle = TextReference.Res(R.string.markets_common_title)
return combine(

View file

@ -153,7 +153,7 @@ private fun Content(state: ChooseTokenFullUM, modifier: Modifier = Modifier) {
)
if (state.marketsBlock != null) {
item("markets_title_spacer") { SpacerH(height = 20.dp) }
item("markets_title_spacer") { SpacerH(height = 40.dp) }
swapMarketsListItems(state.marketsBlock)
}
}
@ -206,9 +206,9 @@ private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapM
private fun LazyListScope.assetsTitle() {
item(key = "assets_title") {
Text(
text = stringResourceSafe(R.string.swap_your_assets_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
text = stringResourceSafe(R.string.markets_portfolio_block_subtitle),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier
.fillMaxWidth()
.padding(
@ -224,7 +224,7 @@ private fun LazyListScope.walletListItem(walletList: WalletListUM) {
if (walletList.items.isEmpty()) return
item("wallet_list") {
LazyRow(
modifier = Modifier.padding(top = 12.dp, bottom = 4.dp),
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
contentPadding = PaddingValues(horizontal = 16.dp),
) {
@ -249,7 +249,7 @@ private fun WalletTabItem(state: WalletTabUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.clip(RoundedCornerShape(12.dp))
.clip(RoundedCornerShape(percent = 50))
.background(backgroundColor)
.clickable(onClick = state.onClick)
.padding(horizontal = 16.dp, vertical = 8.dp),
@ -259,7 +259,7 @@ private fun WalletTabItem(state: WalletTabUM, modifier: Modifier = Modifier) {
Text(
text = state.text.resolveReference(),
color = buttonTextColor,
style = TangemTheme.typography.button,
style = TangemTheme.typography2.bodySemibold16,
)
val count = state.count

View file

@ -2,6 +2,8 @@ package com.tangem.features.commonfeatures.impl.choosetoken.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -14,10 +16,12 @@ import com.tangem.common.ui.markets.MarketsListItem
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.tabs.TangemTab
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketCategoriesUM
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState
internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) {
@ -26,17 +30,22 @@ internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) {
Text(
text = buildAnnotatedString {
append(state.marketsTitle.resolveReference())
if (totalCount != null) {
if (totalCount != null && state.shouldAssetsCount) {
withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) {
append(" $totalCount")
}
}
},
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier.fillMaxWidth().padding(horizontal = TangemTheme.dimens.spacing16),
)
}
state.categories?.let { categories ->
item(key = "market_categories") {
MarketCategoriesRow(categories = categories)
}
}
when (state) {
is SwapMarketState.Loading -> {
items(count = 100, key = { "market_placeholder_$it" }) {
@ -69,7 +78,7 @@ internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) {
modifier = Modifier.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = state.items.lastIndex,
backgroundColor = TangemTheme.colors.background.action,
backgroundColor = TangemTheme.colors.background.primary,
),
)
}
@ -77,6 +86,31 @@ internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) {
}
}
@Composable
private fun MarketCategoriesRow(categories: SwapMarketCategoriesUM, modifier: Modifier = Modifier) {
LazyRow(
modifier = modifier
.fillMaxWidth()
.padding(
top = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing8,
),
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
items(
items = categories.items,
key = { category -> category.name },
) { category ->
TangemTab(
text = category.title,
isChecked = category == categories.selected,
onCheckedChange = { categories.onCategoryClick(category) },
)
}
}
}
@Composable
private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifier) {
Box(

View file

@ -199,7 +199,7 @@ internal class DefaultManageFundsComponent @AssistedInject constructor(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_arrow_back_28),
onClick = onBackClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
variant = TangemButton.Variant.Secondary,
)
}
} else {
@ -211,7 +211,7 @@ internal class DefaultManageFundsComponent @AssistedInject constructor(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24),
onClick = onCloseClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
variant = TangemButton.Variant.Secondary,
)
},
)