Updated on 2026-08-14
This commit is contained in:
parent
73ca4410e1
commit
ab22a6c131
13 changed files with 191 additions and 67 deletions
|
|
@ -14,11 +14,13 @@ sealed class BatchFetchResult<out TData> {
|
|||
* @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<TData>(
|
||||
val data: TData,
|
||||
val empty: Boolean,
|
||||
val last: Boolean,
|
||||
val total: Int? = null,
|
||||
) : BatchFetchResult<TData>()
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
private val updateFetcher: BatchUpdateFetcher<TKey, TData, TUpdate>? = null,
|
||||
) : BatchListSource<TKey, TData, TUpdate> {
|
||||
|
||||
override val state = MutableStateFlow(BatchListState<TKey, TData>(emptyList(), PaginationStatus.None))
|
||||
override val state = MutableStateFlow(BatchListState<TKey, TData>(emptyList(), PaginationStatus.None, null))
|
||||
override val updateResults = MutableSharedFlow<Pair<TUpdate, BatchUpdateResult<TKey, TData>>>(
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
|
|
@ -254,6 +254,7 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
} else {
|
||||
PaginationStatus.Paginating(res)
|
||||
},
|
||||
totalCount = res.total,
|
||||
)
|
||||
}
|
||||
is BatchFetchResult.Error -> {
|
||||
|
|
@ -262,6 +263,7 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
status = PaginationStatus.InitialLoadingError(
|
||||
throwable = res.throwable,
|
||||
),
|
||||
totalCount = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TKey, TData>(
|
||||
val data: List<Batch<TKey, TData>>,
|
||||
val status: PaginationStatus<TData>,
|
||||
val totalCount: Int? = null,
|
||||
)
|
||||
|
|
@ -98,6 +98,7 @@ internal class DefaultMarketsTokenRepository(
|
|||
data = tokenMarketListWithMaxApy.tokens,
|
||||
last = isLast,
|
||||
empty = res.tokens.isEmpty(),
|
||||
total = res.total,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,17 +41,25 @@ internal data class TokenBalanceData(
|
|||
internal sealed interface TokenListUMData {
|
||||
|
||||
val tokensList: ImmutableList<TokensListItemUM>
|
||||
val totalTokensCount: Int
|
||||
|
||||
data class AccountList(
|
||||
override val tokensList: ImmutableList<TokensListItemUM.Portfolio>,
|
||||
override val totalTokensCount: Int,
|
||||
) : TokenListUMData
|
||||
|
||||
data class TokenList(
|
||||
override val tokensList: ImmutableList<TokensListItemUM>,
|
||||
override val totalTokensCount: Int,
|
||||
) : TokenListUMData
|
||||
|
||||
data object EmptyList : TokenListUMData {
|
||||
override val tokensList: ImmutableList<TokensListItemUM> = persistentListOf()
|
||||
override val totalTokensCount: Int = EMPTY_TOKENS_COUNT
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val EMPTY_TOKENS_COUNT = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,15 @@ internal class SwapMarketsListBatchFlowManager(
|
|||
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 }
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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,
|
||||
|
|
|
|||
|
|
@ -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<TokensListItemUM>, isBalanceHidden: Boolean) {
|
||||
itemsIndexed(
|
||||
items = items,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue