diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt index f7b78b226f..eb7a80bd00 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -14,11 +14,13 @@ sealed class BatchFetchResult { * @param data fetched data. * @param empty indicates that data is empty and [BatchListSource] shouldn't create new batch for this result * @param last indicates if this is the last batch for the request. + * @param total total number of items available on the server. */ data class Success( val data: TData, val empty: Boolean, val last: Boolean, + val total: Int? = null, ) : BatchFetchResult() /** diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index dab020caa2..0e70379d40 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -69,7 +69,7 @@ private class DefaultBatchListSource private val updateFetcher: BatchUpdateFetcher? = null, ) : BatchListSource { - override val state = MutableStateFlow(BatchListState(emptyList(), PaginationStatus.None)) + override val state = MutableStateFlow(BatchListState(emptyList(), PaginationStatus.None, null)) override val updateResults = MutableSharedFlow>>( extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, @@ -254,6 +254,7 @@ private class DefaultBatchListSource } else { PaginationStatus.Paginating(res) }, + totalCount = res.total, ) } is BatchFetchResult.Error -> { @@ -262,6 +263,7 @@ private class DefaultBatchListSource status = PaginationStatus.InitialLoadingError( throwable = res.throwable, ), + totalCount = null, ) } } diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt index b53ba23dc0..e752da816b 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt @@ -8,10 +8,12 @@ package com.tangem.pagination * * @property data list of loaded batches. * @property status current status of the pagination. + * @property totalCount total number of items available on the server. * * @see BatchListSource */ data class BatchListState( val data: List>, val status: PaginationStatus, + val totalCount: Int? = null, ) \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index d6df4d9333..9c1b3be3f1 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -98,6 +98,7 @@ internal class DefaultMarketsTokenRepository( data = tokenMarketListWithMaxApy.tokens, last = isLast, empty = res.tokens.isEmpty(), + total = res.total, ) } }, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt index a25fa8ec5c..51fe76f5af 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt @@ -48,22 +48,28 @@ internal class UpdateAccountTokenListTransformer( tokensList = accountListItemConverter.convertList(accountList).toPersistentList(), ) } else { - TokenListUMData.TokenList( - tokensList = persistentListOf( - TokensListItemUM.GroupTitle( - id = "available_tokens_title", - text = resourceReference(R.string.exchange_tokens_available_tokens_header), - ), - ) + accountList.flatMap { (_, currencyList) -> - currencyList.asSequence().map { (isAvailable, status) -> - if (isAvailable) { - availableConverter.convert(status) - } else { - unavailableConverter.convert(status) - } - }.map(TokensListItemUM::Token).toPersistentList() - }.toPersistentList(), - ) + val tokensList = accountList.flatMap { (_, currencyList) -> + currencyList.asSequence().map { (isAvailable, status) -> + if (isAvailable) { + availableConverter.convert(status) + } else { + unavailableConverter.convert(status) + } + }.map(TokensListItemUM::Token).toPersistentList() + }.toPersistentList() + + if (tokensList.isNotEmpty()) { + TokenListUMData.TokenList( + tokensList = persistentListOf( + TokensListItemUM.GroupTitle( + id = "available_tokens_title", + text = resourceReference(R.string.exchange_tokens_available_tokens_header), + ), + ) + tokensList, + ) + } else { + TokenListUMData.EmptyList + } }, isBalanceHidden = isBalanceHidden, warning = warning, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt index e7b9c5ee8c..af538747a6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt @@ -37,26 +37,36 @@ internal class TokensDataConverterV2( availableTokens = persistentListOf(), unavailableTokens = persistentListOf(), tokensListData = if (isAccountsMode) { + val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList() + val totalTokensCount = portfolioList.sumOf { it.tokens.size } TokenListUMData.AccountList( - tokensList = accountListItemConverter.convertList(accountList).toPersistentList(), + tokensList = portfolioList, + totalTokensCount = totalTokensCount, ) } else { - TokenListUMData.TokenList( - tokensList = persistentListOf( - TokensListItemUM.GroupTitle( - id = "available_tokens_title", - text = resourceReference(R.string.exchange_tokens_available_tokens_header), - ), - ) + accountList.flatMap { (_, currencyList) -> - currencyList.asSequence().map { accountSwapCurrency -> - if (accountSwapCurrency.isAvailable) { - accountListItemConverter.createAvailableItemConverter() - } else { - accountListItemConverter.createUnavailableItemConverter() - }.convert(accountSwapCurrency.cryptoCurrencyStatus) - }.map(TokensListItemUM::Token).toPersistentList() - }.toPersistentList(), - ) + val tokensList = accountList.flatMap { (_, currencyList) -> + currencyList.asSequence().map { accountSwapCurrency -> + if (accountSwapCurrency.isAvailable) { + accountListItemConverter.createAvailableItemConverter() + } else { + accountListItemConverter.createUnavailableItemConverter() + }.convert(accountSwapCurrency.cryptoCurrencyStatus) + }.map(TokensListItemUM::Token).toPersistentList() + }.toPersistentList() + + if (tokensList.isNotEmpty()) { + TokenListUMData.TokenList( + tokensList = persistentListOf( + TokensListItemUM.GroupTitle( + id = "available_tokens_title", + text = resourceReference(R.string.exchange_tokens_available_tokens_header), + ), + ) + tokensList, + totalTokensCount = tokensList.size, + ) + } else { + TokenListUMData.EmptyList + } }, marketsState = currentMarketsState, onSearchEntered = onSearchEntered, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index ce909de7e6..42b072c45d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -261,6 +261,13 @@ internal class SwapModel @Inject constructor( override fun onSuccess(addedToken: CryptoCurrency) { modelScope.launch { bottomSheetNavigation.dismiss() + uiState.selectTokenState?.let { currentSelectState -> + uiState = uiState.copy( + selectTokenState = currentSelectState.copy( + marketsState = null, + ), + ) + } getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken) .firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded @@ -341,7 +348,8 @@ internal class SwapModel @Inject constructor( flow2 = searchMarketsListManager.uiItems, flow3 = searchMarketsListManager.isInInitialLoadingErrorState, flow4 = searchMarketsListManager.isSearchNotFoundState, - ) { searchQuery, uiItems, isError, isSearchNotFound -> + flow5 = searchMarketsListManager.totalCount.filterNotNull(), + ) { searchQuery, uiItems, isError, isSearchNotFound, total -> when { searchQuery.isEmpty() -> { visibleMarketItemIds.value = emptyList() @@ -359,6 +367,7 @@ internal class SwapModel @Inject constructor( addToPortfolioItem(item) }, visibleIdsChanged = { visibleMarketItemIds.value = it }, + total = total, ) } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index c6408b6467..96d1358d0a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -41,17 +41,25 @@ internal data class TokenBalanceData( internal sealed interface TokenListUMData { val tokensList: ImmutableList + val totalTokensCount: Int data class AccountList( override val tokensList: ImmutableList, + override val totalTokensCount: Int, ) : TokenListUMData data class TokenList( override val tokensList: ImmutableList, + override val totalTokensCount: Int, ) : TokenListUMData data object EmptyList : TokenListUMData { override val tokensList: ImmutableList = persistentListOf() + override val totalTokensCount: Int = EMPTY_TOKENS_COUNT + } + + private companion object { + const val EMPTY_TOKENS_COUNT = 0 } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt index 7070acfa61..3e7b9af39f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt @@ -78,6 +78,15 @@ internal class SwapMarketsListBatchFlowManager( initialValue = false, ) + val totalCount: StateFlow = batchFlow.state + .map { it.totalCount } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + init { batchFlow.state .map { it.data } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt index 8a84fb4d8b..50e6d14a74 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt @@ -10,6 +10,7 @@ internal sealed class SwapMarketState { data class Content( val items: ImmutableList, + val total: Int, val loadMore: () -> Unit, val onItemClick: (MarketsListItemUM) -> Unit, val visibleIdsChanged: (List) -> Unit, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index bd038744a8..2bc3cc5c61 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -23,8 +23,11 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -73,6 +76,11 @@ internal fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () when { state.isNotFoundState -> TokensNotFound(modifier) state.isEmptyState -> EmptyTokensList(modifier) + state.marketsState != null -> ListOfTokensWithMarkets( + state = state, + marketsState = state.marketsState, + modifier = modifier, + ) else -> ListOfTokens(state = state, modifier = modifier) } }, @@ -142,6 +150,34 @@ private fun TokensNotFound(modifier: Modifier = Modifier) { @Composable private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) { val screenBackgroundColor = TangemTheme.colors.background.secondary + + LazyColumn( + modifier = modifier + .background(color = screenBackgroundColor) + .fillMaxSize() + .imePadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + tokensListItems( + tokensListData = state.tokensListData, + isBalanceHidden = state.isBalanceHidden, + ) + + tokensToSelectItems(state.availableTokens, state.onTokenSelected) + + item { SpacerH12() } + + tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) + } +} + +@Composable +private fun ListOfTokensWithMarkets( + state: SwapSelectTokenStateHolder, + marketsState: SwapMarketState, + modifier: Modifier = Modifier, +) { + val screenBackgroundColor = TangemTheme.colors.background.secondary val lazyListState = rememberLazyListState() LazyColumn( @@ -152,54 +188,37 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = horizontalAlignment = Alignment.CenterHorizontally, state = lazyListState, ) { - when (val list = state.tokensListData) { - is TokenListUMData.AccountList -> { - list.tokensList.forEach { item -> - portfolioTokensList( - portfolio = item, - isBalanceHidden = state.isBalanceHidden, - ) - } - } - is TokenListUMData.TokenList -> { - tokensList( - items = list.tokensList, - isBalanceHidden = state.isBalanceHidden, - ) - } - TokenListUMData.EmptyList -> Unit + if (state.tokensListData !is TokenListUMData.EmptyList) { + assetsTitle(count = state.tokensListData.totalTokensCount) } - if (state.availableTokens.isNotEmpty()) { - tokensToSelectItems(state.availableTokens, state.onTokenSelected) - } - if (state.unavailableTokens.isNotEmpty()) { + tokensListItems( + tokensListData = state.tokensListData, + isBalanceHidden = state.isBalanceHidden, + ) + + if (state.tokensListData is TokenListUMData.EmptyList) { item { SpacerH12() } - tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) - } - - if (state.availableTokens.isNotEmpty() || state.unavailableTokens.isNotEmpty()) { - item { SpacerH32() } } else { - item { SpacerH12() } + item { SpacerH32() } } - state.marketsState?.let(::swapMarketsListItems) + swapMarketsListItems(marketsState) } - (state.marketsState as? SwapMarketState.Content)?.let { marketState -> + (marketsState as? SwapMarketState.Content)?.let { content -> VisibleItemsTracker( lazyListState = lazyListState, - marketState = marketState, + marketState = content, ) InfiniteListHandler( listState = lazyListState, buffer = LOAD_MORE_BUFFER, triggerLoadMoreCheckOnItemsCountChange = true, - onLoadMore = remember(state) { + onLoadMore = remember(content) { { - state.marketsState.loadMore() + content.loadMore() true } }, @@ -223,6 +242,48 @@ private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapM } } +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.tokensListItems(tokensListData: TokenListUMData, isBalanceHidden: Boolean) { + when (tokensListData) { + is TokenListUMData.AccountList -> { + tokensListData.tokensList.forEach { item -> + portfolioTokensList( + portfolio = item, + isBalanceHidden = isBalanceHidden, + ) + } + } + is TokenListUMData.TokenList -> { + tokensList( + items = tokensListData.tokensList, + isBalanceHidden = isBalanceHidden, + ) + } + TokenListUMData.EmptyList -> Unit + } +} + private fun LazyListScope.tokensList(items: ImmutableList, isBalanceHidden: Boolean) { itemsIndexed( items = items, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt index 0e7fb5e9bb..0f69e31f8b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt @@ -7,6 +7,9 @@ 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.components.UnableToLoadData @@ -18,8 +21,16 @@ import com.tangem.feature.swap.presentation.R internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) { item { + val totalCount = (state as? SwapMarketState.Content)?.total Text( - text = stringResourceSafe(R.string.markets_common_title), + 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), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt index a0c8c9a1b1..b9a6ab2d40 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt @@ -34,6 +34,7 @@ internal class SwapSelectTokenPreviewProvider { loadMore = { }, onItemClick = { }, visibleIdsChanged = { }, + total = TOTAL_ITEMS, ) private fun createPreviewMarketItems() = listOf( @@ -114,6 +115,7 @@ internal class SwapSelectTokenPreviewProvider { private const val CHART_VALUE_4 = 2.0 private const val CHART_VALUE_5 = 5.0 private const val CHART_VALUE_6 = 3.0 + private const val TOTAL_ITEMS = 322 private val PREVIEW_CHART_DATA = MarketChartRawData( y = persistentListOf(