From 82ae40d9f7cb20099b8169cb1447375c025f8b93 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Jul 2026 18:12:18 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../market/state/SwapMarketCategory.kt | 8 ++++ .../choosetoken/model/MarketBlockDelegate.kt | 24 ++++++++--- .../model/MarketBlockDelegateTest.kt | 40 +++++++++++++++++++ 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/market/state/SwapMarketCategory.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/market/state/SwapMarketCategory.kt index 926aeecb47..234641740d 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/market/state/SwapMarketCategory.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/market/state/SwapMarketCategory.kt @@ -28,6 +28,14 @@ internal enum class SwapMarketCategory( title = resourceReference(R.string.markets_sort_by_top_losers_title), order = TokenMarketListConfig.Order.TopLosers, ), + ExperiencedBuyers( + title = resourceReference(R.string.markets_sort_by_experienced_buyers_title), + order = TokenMarketListConfig.Order.Buyers, + ), + Trending( + title = resourceReference(R.string.markets_sort_by_trending_title), + order = TokenMarketListConfig.Order.Trending, + ), } /** diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt index b5f33f004b..b5e472f1af 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt @@ -34,6 +34,8 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* +private const val MARKET_PULSE_ITEM_LIMIT = 5 + @Suppress("LongParameterList") internal class MarketBlockDelegate @AssistedInject constructor( private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory, @@ -84,7 +86,7 @@ internal class MarketBlockDelegate @AssistedInject constructor( * When single-currency wallets aren't selectable here (e.g. swap), the wallet is always * multi-currency, so we skip the per-wallet logic entirely and return [baseMarketsStateFlow]. */ - val marketsStateFlow: Flow = if (!shouldShowSingleCurrencyWallets) { + private val walletAwareMarketsStateFlow: Flow = if (!shouldShowSingleCurrencyWallets) { baseMarketsStateFlow } else { selectedWalletFlow @@ -92,6 +94,10 @@ internal class MarketBlockDelegate @AssistedInject constructor( .distinctUntilChanged() } + val marketsStateFlow: Flow = walletAwareMarketsStateFlow + .map { it.limitMarketPulseItems() } + .distinctUntilChanged() + private val defaultMarketsListManager by lazy { marketsListBatchFlowManagerFactory.create( batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, @@ -156,9 +162,8 @@ internal class MarketBlockDelegate @AssistedInject constructor( return combine( flow = defaultMarketsListManager.uiItems, flow2 = defaultMarketsListManager.isInInitialLoadingErrorState, - flow3 = defaultMarketsListManager.totalCount, - flow4 = selectedCategoryFlow, - ) { uiItems, isError, total, selectedCategory -> + flow3 = selectedCategoryFlow, + ) { uiItems, isError, selectedCategory -> val categories = buildCategoriesUM(selectedCategory) when { isError -> SwapMarketState.LoadingError( @@ -174,10 +179,10 @@ internal class MarketBlockDelegate @AssistedInject constructor( ) else -> SwapMarketState.Content( items = uiItems, - loadMore = { defaultMarketsListManager.loadMore() }, + loadMore = {}, onItemClick = { item -> addToPortfolioItem(item) }, visibleIdsChanged = { visibleDefaultMarketItemIds.value = it }, - total = total ?: uiItems.size, + total = uiItems.size, marketsTitle = marketsTitle, shouldAssetsCount = false, categories = categories, @@ -265,6 +270,13 @@ internal class MarketBlockDelegate @AssistedInject constructor( } } + private fun SwapMarketState?.limitMarketPulseItems(): SwapMarketState? { + if (this !is SwapMarketState.Content || shouldAssetsCount) return this + if (items.size <= MARKET_PULSE_ITEM_LIMIT) return this + val limitedItems = items.take(MARKET_PULSE_ITEM_LIMIT).toImmutableList() + return copy(items = limitedItems, total = limitedItems.size) + } + private fun addToPortfolioItem(item: MarketsListItemUM) { val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) ?: searchMarketsListManager.getTokenMarketById(item.id) ?: return diff --git a/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegateTest.kt b/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegateTest.kt index 338914683e..29a89e65ac 100644 --- a/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegateTest.kt +++ b/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegateTest.kt @@ -27,6 +27,7 @@ import io.mockk.every import io.mockk.mockk import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -116,6 +117,23 @@ internal class MarketBlockDelegateTest { assertThat((result as SwapMarketState.Content).items).containsExactly(item1, item2).inOrder() } + @Test + fun `GIVEN more than 5 market items WHEN default flow emitted THEN only first 5 shown`() = runTest { + // Arrange + val items = (1..7).map { marketItem("token-$it") } + defaultUiItems.value = items.toPersistentList() + val delegate = createDelegate(wallet = MockUserWalletFactory.create()) + + // Act + val result = lastMarketState(delegate) + + // Assert + assertThat(result).isInstanceOf(SwapMarketState.Content::class.java) + val content = result as SwapMarketState.Content + assertThat(content.items).containsExactlyElementsIn(items.take(5)).inOrder() + assertThat(content.total).isEqualTo(5) + } + @Test fun `GIVEN single-currency wallet WHEN trending emitted THEN market block is hidden`() = runTest { // Arrange @@ -188,6 +206,28 @@ internal class MarketBlockDelegateTest { assertThat(result).isNull() } + @Test + fun `GIVEN NODL wallet WHEN network token is beyond first 5 THEN block still shows it`() = runTest { + // Arrange — 5 tokens on another network first, the wallet-network token only at position 6. + val nodlWallet = MockUserWalletFactory.createSingleWalletWithToken() + val otherNetworkItems = (1..5).map { marketItem("token-eth-$it") } + val walletNetworkItem = marketItem("token-stellar") + (1..5).forEach { tokenMarketsByRawId["token-eth-$it"] = tokenMarket(ETHEREUM_NETWORK_ID) } + tokenMarketsByRawId["token-stellar"] = tokenMarket(STELLAR_NETWORK_ID) + defaultUiItems.value = (otherNetworkItems + walletNetworkItem).toPersistentList() + + every { + singleAccountStatusListSupplier(nodlWallet.walletId) + } returns flowOf(accountStatusList(STELLAR_NETWORK_ID)) + + // Act + val result = lastMarketState(createDelegate(wallet = nodlWallet)) + + // Assert — network filtering runs on the full list before the 5-item cap, so it isn't dropped. + assertThat(result).isInstanceOf(SwapMarketState.Content::class.java) + assertThat((result as SwapMarketState.Content).items).containsExactly(walletNetworkItem) + } + // region Helpers private fun TestScope.lastMarketState(delegate: MarketBlockDelegate): SwapMarketState? {