Updated on 2026-08-14
This commit is contained in:
parent
4ec02d0447
commit
f426a1f922
26 changed files with 748 additions and 247 deletions
|
|
@ -5,7 +5,7 @@ import java.math.BigDecimal
|
|||
|
||||
data class TokenMarketListResponse(
|
||||
@Json(name = "imageHost")
|
||||
val imageHost: String,
|
||||
val imageHost: String?,
|
||||
@Json(name = "tokens")
|
||||
val tokens: List<Token>,
|
||||
@Json(name = "total")
|
||||
|
|
|
|||
|
|
@ -68,4 +68,10 @@ sealed class BatchAction<out TKey, out TRequestParams, out TUpdate> {
|
|||
class CancelUpdates<TKey, TUpdate>(
|
||||
val predicate: (UpdateBatches<TKey, TUpdate>) -> Boolean,
|
||||
) : BatchAction<TKey, Nothing, TUpdate>()
|
||||
|
||||
/**
|
||||
* Clears the state and stops all current batch loading and updates
|
||||
* After this status becomes [PaginationStatus.None]
|
||||
*/
|
||||
data object Reset : BatchAction<Nothing, Nothing, Nothing>()
|
||||
}
|
||||
|
|
@ -12,10 +12,12 @@ sealed class BatchFetchResult<out TData> {
|
|||
* Represents a successful result of a batch fetch request.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
data class Success<TData>(
|
||||
val data: TData,
|
||||
val empty: Boolean,
|
||||
val last: Boolean,
|
||||
) : BatchFetchResult<TData>()
|
||||
|
||||
|
|
|
|||
|
|
@ -92,11 +92,7 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
awaitCancellation()
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
stopAllUpdates()
|
||||
loadMoreActionJob = null
|
||||
loadMoreActionJob = null
|
||||
lastRequestResult.value = null
|
||||
state.value = BatchListState(emptyList(), PaginationStatus.None)
|
||||
resetState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,6 +106,7 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun collectActions(action: BatchAction<TKey, TRequestParams, TUpdate>) {
|
||||
when (action) {
|
||||
is BatchAction.Reload -> {
|
||||
|
|
@ -165,6 +162,9 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
loadMoreActionJob?.cancel()
|
||||
reloadActionJob?.cancel()
|
||||
}
|
||||
BatchAction.Reset -> {
|
||||
resetState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -237,13 +237,17 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
|
||||
state.value = when (res) {
|
||||
is BatchFetchResult.Success -> {
|
||||
val key = generateNewKey(listOf())
|
||||
val batch = Batch(
|
||||
key = key,
|
||||
data = res.data,
|
||||
)
|
||||
val batch = if (res.empty.not()) {
|
||||
Batch(
|
||||
key = generateNewKey(listOf()),
|
||||
data = res.data,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
BatchListState(
|
||||
data = listOf(batch),
|
||||
data = batch?.let { listOf(it) } ?: emptyList(),
|
||||
status = if (res.last) {
|
||||
PaginationStatus.EndOfPagination
|
||||
} else {
|
||||
|
|
@ -289,13 +293,17 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
state.update { currentState ->
|
||||
when (res) {
|
||||
is BatchFetchResult.Success -> {
|
||||
val newBatch = Batch(
|
||||
key = generateNewKey(currentState.data.map { it.key }),
|
||||
data = res.data,
|
||||
)
|
||||
val newBatch = if (res.empty.not()) {
|
||||
Batch(
|
||||
key = generateNewKey(currentState.data.map { it.key }),
|
||||
data = res.data,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
currentState.copy(
|
||||
data = currentState.data + newBatch,
|
||||
data = newBatch?.let { currentState.data + it } ?: currentState.data,
|
||||
status = if (res.last) {
|
||||
PaginationStatus.EndOfPagination
|
||||
} else {
|
||||
|
|
@ -400,6 +408,18 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
waitingUpdateJobs.value.any { it.first.operationId == operationId }
|
||||
}
|
||||
|
||||
private fun resetState() {
|
||||
if (updateFetcher != null) {
|
||||
stopAllUpdates()
|
||||
}
|
||||
loadMoreActionJob?.cancel()
|
||||
loadMoreActionJob = null
|
||||
reloadActionJob?.cancel()
|
||||
reloadActionJob = null
|
||||
lastRequestResult.value = null
|
||||
state.value = BatchListState(emptyList(), PaginationStatus.None)
|
||||
}
|
||||
|
||||
private fun stopAllUpdates() {
|
||||
updateAsyncJobs.update { actionAsyncJobs ->
|
||||
actionAsyncJobs.forEach {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class LimitOffsetBatchFetcher<TRequestParams : Any, TData>(
|
|||
suspend fun fetch(
|
||||
request: Request<TRequestParams>,
|
||||
lastResult: BatchFetchResult<TData>?,
|
||||
isFirstBatchFetching: Boolean,
|
||||
): BatchFetchResult<TData>
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +46,7 @@ class LimitOffsetBatchFetcher<TRequestParams : Any, TData>(
|
|||
)
|
||||
|
||||
val res = runCatching {
|
||||
subFetcher.fetch(req, null)
|
||||
subFetcher.fetch(request = req, lastResult = null, isFirstBatchFetching = true)
|
||||
}.getOrElse {
|
||||
currentCoroutineContext().ensureActive()
|
||||
BatchFetchResult.Error(it)
|
||||
|
|
@ -77,7 +78,7 @@ class LimitOffsetBatchFetcher<TRequestParams : Any, TData>(
|
|||
}
|
||||
|
||||
val res = runCatching {
|
||||
subFetcher.fetch(req, lastResult)
|
||||
subFetcher.fetch(request = req, lastResult = lastResult, isFirstBatchFetching = false)
|
||||
}.getOrElse {
|
||||
currentCoroutineContext().ensureActive()
|
||||
BatchFetchResult.Error(it)
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@
|
|||
<string name="cardano_max_amount_has_token_title">Недостаточно ADA</string>
|
||||
<string name="common_accept">Принять</string>
|
||||
<string name="common_access_denied">Доступ запрещен</string>
|
||||
<string name="common_allow">Разрешить</string>
|
||||
<string name="common_apply">Применить</string>
|
||||
<string name="common_approval">Одобрение</string>
|
||||
<string name="common_approve">Подтвердить</string>
|
||||
|
|
|
|||
|
|
@ -337,7 +337,12 @@
|
|||
<string name="markets_common_my_portfolio">My portfolio</string>
|
||||
<string name="markets_common_title">Market</string>
|
||||
<string name="markets_generate_addresses_notification">To generate addresses for selected networks, you need to attach a Tangem card</string>
|
||||
<string name="markets_loading_error_title">Unable to load the data…</string>
|
||||
<string name="markets_quick_actions">Quick actions</string>
|
||||
<string name="markets_search_result_title">Result</string>
|
||||
<string name="markets_search_see_tokens_under_100k">See tokens under 100k market cap</string>
|
||||
<string name="markets_search_show_tokens">Show tokens</string>
|
||||
<string name="markets_search_token_no_result_title">No result</string>
|
||||
<string name="markets_select_network">Select network</string>
|
||||
<string name="markets_select_wallet">Select wallet</string>
|
||||
<string name="markets_selector_interval_1m_title">1m</string>
|
||||
|
|
@ -722,6 +727,7 @@
|
|||
<string name="transaction_history_operation">Operation</string>
|
||||
<string name="transaction_history_transaction_from_address">from: %s</string>
|
||||
<string name="transaction_history_transaction_to_address">to: %s</string>
|
||||
<string name="try_to_load_data_again_button_title">Try again</string>
|
||||
<string name="twin_error_same_card">You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d</string>
|
||||
<string name="twin_error_wrong_twin">You\'ve scanned wrong twin card. Please try another one</string>
|
||||
<string name="twins_onboarding_description_format">This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet.</string>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
|||
import com.tangem.pagination.*
|
||||
import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
internal class DefaultMarketsTokenRepository(
|
||||
private val marketsApi: TangemTechMarketsApi,
|
||||
|
|
@ -20,42 +20,61 @@ internal class DefaultMarketsTokenRepository(
|
|||
|
||||
private val tokenListConverter = TokenMarketListConverter()
|
||||
|
||||
private val tokenMarketsFetcher
|
||||
get() = LimitOffsetBatchFetcher(
|
||||
prefetchDistance = 150,
|
||||
batchSize = 100,
|
||||
subFetcher = object : LimitOffsetBatchFetcher.SubFetcher<TokenMarketListConfig, List<TokenMarket>> {
|
||||
private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher(
|
||||
prefetchDistance = firstBatchSize,
|
||||
batchSize = nextBatchSize,
|
||||
subFetcher = object : LimitOffsetBatchFetcher.SubFetcher<TokenMarketListConfig, List<TokenMarket>> {
|
||||
|
||||
var requestTimeStamp: Long? = null // TODO when backend is ready
|
||||
val requestTimeStamp = AtomicLong(0)
|
||||
|
||||
override suspend fun fetch(
|
||||
request: LimitOffsetBatchFetcher.Request<TokenMarketListConfig>,
|
||||
lastResult: BatchFetchResult<List<TokenMarket>>?,
|
||||
): BatchFetchResult<List<TokenMarket>> {
|
||||
val res = retryOnError(priority = true) {
|
||||
marketsApi.getCoinsList(
|
||||
currency = request.params.fiatPriceCurrency,
|
||||
interval = request.params.priceChangeInterval.toRequestParam(),
|
||||
order = request.params.order.toRequestParam(),
|
||||
search = request.params.searchText,
|
||||
generalCoins = request.params.showUnder100kMarketCapTokens.not(),
|
||||
offset = request.offset,
|
||||
limit = request.limit,
|
||||
).getOrThrow()
|
||||
}
|
||||
override suspend fun fetch(
|
||||
request: LimitOffsetBatchFetcher.Request<TokenMarketListConfig>,
|
||||
lastResult: BatchFetchResult<List<TokenMarket>>?,
|
||||
isFirstBatchFetching: Boolean,
|
||||
): BatchFetchResult<List<TokenMarket>> {
|
||||
val searchText =
|
||||
if (request.params.searchText.isNullOrBlank()) null else request.params.searchText
|
||||
|
||||
val last = res.tokens.size < request.limit
|
||||
|
||||
return BatchFetchResult.Success(
|
||||
data = tokenListConverter.convert(res),
|
||||
last = last,
|
||||
)
|
||||
val requestCall = suspend {
|
||||
marketsApi.getCoinsList(
|
||||
currency = request.params.fiatPriceCurrency,
|
||||
interval = request.params.priceChangeInterval.toRequestParam(),
|
||||
order = request.params.order.toRequestParam(),
|
||||
search = searchText,
|
||||
generalCoins = request.params.showUnder100kMarketCapTokens.not(),
|
||||
offset = request.offset,
|
||||
limit = request.limit,
|
||||
).getOrThrow()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// we shouldn't infinitely retry on the first batch request
|
||||
val res = if (isFirstBatchFetching) {
|
||||
requestCall()
|
||||
} else {
|
||||
retryOnError(priority = true) {
|
||||
requestCall()
|
||||
}
|
||||
}
|
||||
|
||||
if (isFirstBatchFetching) {
|
||||
requestTimeStamp.set(0) // TODO when backend is ready
|
||||
}
|
||||
|
||||
val last = res.tokens.size < request.limit
|
||||
|
||||
return BatchFetchResult.Success(
|
||||
data = tokenListConverter.convert(res),
|
||||
last = last,
|
||||
empty = res.tokens.isEmpty(),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
override fun getTokenListFlow(
|
||||
batchingContext: BatchingContext<Int, TokenMarketListConfig, TokenMarketUpdateRequest>,
|
||||
firstBatchSize: Int,
|
||||
nextBatchSize: Int,
|
||||
): BatchFlow<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
|
||||
val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher(
|
||||
tangemTechApi = tangemTechApi,
|
||||
|
|
@ -66,7 +85,7 @@ internal class DefaultMarketsTokenRepository(
|
|||
fetchDispatcher = dispatcherProvider.io,
|
||||
context = batchingContext,
|
||||
generateNewKey = { it.size },
|
||||
batchFetcher = tokenMarketsFetcher,
|
||||
batchFetcher = createTokenMarketsFetcher(firstBatchSize = firstBatchSize, nextBatchSize = nextBatchSize),
|
||||
updateFetcher = tokenMarketsUpdateFetcher,
|
||||
).toBatchFlow()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ import com.tangem.utils.converter.Converter
|
|||
class TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMarket>> {
|
||||
|
||||
override fun convert(value: TokenMarketListResponse): List<TokenMarket> {
|
||||
val imageHost = value.imageHost ?: run {
|
||||
if (value.tokens.isEmpty()) {
|
||||
return emptyList()
|
||||
} else {
|
||||
error("imageHost cannot be null")
|
||||
}
|
||||
}
|
||||
|
||||
return value.tokens.map { token ->
|
||||
TokenMarket(
|
||||
id = token.id,
|
||||
|
|
@ -16,7 +24,7 @@ class TokenMarketListConverter : Converter<TokenMarketListResponse, List<TokenMa
|
|||
symbol = token.symbol,
|
||||
marketRating = token.marketRating,
|
||||
marketCap = token.marketCap,
|
||||
imageHost = value.imageHost,
|
||||
imageHost = imageHost,
|
||||
tokenQuotes = TokenQuotes(
|
||||
currentPrice = token.currentPrice,
|
||||
priceChanges = mapOf(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,19 @@ typealias TokenListBatchFlow = BatchFlow<Int, List<TokenMarket>, TokenMarketUpda
|
|||
class GetMarketsTokenListFlowUseCase(
|
||||
private val marketsTokenRepository: MarketsTokenRepository,
|
||||
) {
|
||||
operator fun invoke(batchingContext: TokenListBatchingContext): TokenListBatchFlow {
|
||||
return marketsTokenRepository.getTokenListFlow(batchingContext)
|
||||
operator fun invoke(batchingContext: TokenListBatchingContext, batchFlowType: BatchFlowType): TokenListBatchFlow {
|
||||
return marketsTokenRepository.getTokenListFlow(
|
||||
batchingContext = batchingContext,
|
||||
firstBatchSize = batchFlowType.firstBatchSize,
|
||||
nextBatchSize = batchFlowType.nextBatchSize,
|
||||
)
|
||||
}
|
||||
|
||||
enum class BatchFlowType(
|
||||
val firstBatchSize: Int,
|
||||
val nextBatchSize: Int,
|
||||
) {
|
||||
Main(firstBatchSize = 150, nextBatchSize = 100),
|
||||
Search(firstBatchSize = 50, nextBatchSize = 50),
|
||||
}
|
||||
}
|
||||
|
|
@ -4,5 +4,9 @@ import com.tangem.domain.markets.*
|
|||
|
||||
interface MarketsTokenRepository {
|
||||
|
||||
fun getTokenListFlow(batchingContext: TokenListBatchingContext): TokenListBatchFlow
|
||||
fun getTokenListFlow(
|
||||
batchingContext: TokenListBatchingContext,
|
||||
firstBatchSize: Int,
|
||||
nextBatchSize: Int,
|
||||
): TokenListBatchFlow
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ dependencies {
|
|||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
||||
/* DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -30,15 +30,17 @@ internal class DefaultMarketsListComponent @AssistedInject constructor(
|
|||
modifier: Modifier,
|
||||
) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bsState by bottomSheetState
|
||||
|
||||
LaunchedEffect(bottomSheetState.value) {
|
||||
model.containerBottomSheetState.value = bottomSheetState.value
|
||||
LaunchedEffect(bsState) {
|
||||
model.containerBottomSheetState.value = bsState
|
||||
}
|
||||
|
||||
MarketsList(
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
bottomSheetState = bsState,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,21 +9,21 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.model.statemanager.MarketsListUMStateManager
|
||||
import com.tangem.features.markets.model.statemanager.MarketsListUiItemsManager
|
||||
import com.tangem.features.markets.model.statemanager.MarketsListBatchFlowManager
|
||||
import com.tangem.features.markets.ui.entity.ListUM
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
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.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
|
||||
private const val SEARCH_QUERY_DEBOUNCE_MILLIS = 800L
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
@ComponentScoped
|
||||
@Stable
|
||||
internal class MarketsListModel @Inject constructor(
|
||||
|
|
@ -42,38 +42,65 @@ internal class MarketsListModel @Inject constructor(
|
|||
)
|
||||
|
||||
private val visibleItemIds = MutableStateFlow<List<String>>(emptyList())
|
||||
val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED)
|
||||
|
||||
private val marketsListUMStateManager = MarketsListUMStateManager(
|
||||
onLoadMoreUiItems = { activeListManager.loadMore() },
|
||||
visibleItemsChanged = { visibleItemIds.value = it },
|
||||
onRetryButtonClicked = { activeListManager.reload() },
|
||||
)
|
||||
|
||||
private val marketsListManager = MarketsListUiItemsManager(
|
||||
logTag = "main",
|
||||
private val mainMarketsListManager = MarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main,
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval },
|
||||
currentSortByType = Provider { marketsListUMStateManager.selectedSortByType },
|
||||
currentSearchText = Provider { null },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
private val searchMarketsListManager = MarketsListUiItemsManager(
|
||||
logTag = "search",
|
||||
private val searchMarketsListManager = MarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
|
||||
batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search,
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval },
|
||||
currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval }, // FIXME fix on backend
|
||||
currentSortByType = Provider { SortByTypeUM.Rating }, // FIXME maybe fix on backend
|
||||
currentSearchText = Provider { marketsListUMStateManager.searchQuery },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
private var activeListManager: MarketsListUiItemsManager = marketsListManager
|
||||
private var activeListManager: MarketsListBatchFlowManager = mainMarketsListManager
|
||||
|
||||
val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED)
|
||||
|
||||
val state = marketsListUMStateManager.state.asStateFlow()
|
||||
|
||||
init {
|
||||
@Suppress("UnnecessaryParentheses")
|
||||
modelScope.launch {
|
||||
marketsListManager.uiItems
|
||||
.collectLatest {
|
||||
marketsListUMStateManager.onUiItemsChanged(it)
|
||||
marketsListUMStateManager.isInSearchStateFlow
|
||||
.flatMapLatest { isInSearchMode ->
|
||||
if (isInSearchMode) {
|
||||
combine(
|
||||
searchMarketsListManager.uiItems,
|
||||
searchMarketsListManager.isInInitialLoadingErrorState,
|
||||
searchMarketsListManager.isSearchNotFoundState,
|
||||
) { items, isError, notFound ->
|
||||
(items to isError) to notFound
|
||||
}
|
||||
} else {
|
||||
combine(
|
||||
mainMarketsListManager.uiItems,
|
||||
mainMarketsListManager.isInInitialLoadingErrorState,
|
||||
) { items, isError -> (items to isError) to false }
|
||||
}
|
||||
}.collect {
|
||||
marketsListUMStateManager.onUiItemsChanged(
|
||||
uiItems = it.first.first,
|
||||
isInErrorState = it.first.second,
|
||||
isSearchNotFound = it.second,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,23 +114,16 @@ internal class MarketsListModel @Inject constructor(
|
|||
currentAppCurrency
|
||||
.drop(1)
|
||||
.onEach {
|
||||
marketsListManager.reload(
|
||||
interval = marketsListUMStateManager.selectedInterval,
|
||||
sortBy = marketsListUMStateManager.selectedSortByType,
|
||||
)
|
||||
mainMarketsListManager.reload()
|
||||
if (marketsListUMStateManager.isInSearchState) {
|
||||
// TODO
|
||||
searchMarketsListManager.reload(
|
||||
interval = marketsListUMStateManager.selectedInterval,
|
||||
sortBy = marketsListUMStateManager.selectedSortByType,
|
||||
)
|
||||
searchMarketsListManager.reload()
|
||||
}
|
||||
}.launchIn(modelScope)
|
||||
|
||||
// load charts when new batch is being loaded
|
||||
marketsListManager.onLastBatchLoadedSuccess
|
||||
mainMarketsListManager.onLastBatchLoadedSuccess
|
||||
.onEach {
|
||||
marketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval)
|
||||
mainMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval)
|
||||
modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
|
|
@ -117,11 +137,11 @@ internal class MarketsListModel @Inject constructor(
|
|||
.collectLatest { interval ->
|
||||
when (marketsListUMStateManager.selectedSortByType) {
|
||||
SortByTypeUM.Rating -> {
|
||||
marketsListManager.updateUIWithSameState()
|
||||
val batchKeys = marketsListManager.getBatchKeysByItemIds(visibleItemIds.value)
|
||||
marketsListManager.loadCharts(batchKeys, interval)
|
||||
mainMarketsListManager.updateUIWithSameState()
|
||||
val batchKeys = mainMarketsListManager.getBatchKeysByItemIds(visibleItemIds.value)
|
||||
mainMarketsListManager.loadCharts(batchKeys, interval)
|
||||
}
|
||||
else -> marketsListManager.reload(interval, marketsListUMStateManager.selectedSortByType)
|
||||
else -> mainMarketsListManager.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -133,12 +153,12 @@ internal class MarketsListModel @Inject constructor(
|
|||
.distinctUntilChanged()
|
||||
.drop(1)
|
||||
.collectLatest {
|
||||
marketsListManager.reload(marketsListUMStateManager.selectedInterval, it)
|
||||
mainMarketsListManager.reload()
|
||||
}
|
||||
}
|
||||
|
||||
// listen current visible batch and update charts
|
||||
modelScope.launch(dispatchers.default) {
|
||||
modelScope.launch {
|
||||
visibleItemIds
|
||||
.mapNotNull {
|
||||
if (it.isNotEmpty()) {
|
||||
|
|
@ -153,11 +173,41 @@ internal class MarketsListModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// ===Search===
|
||||
|
||||
modelScope.launch {
|
||||
marketsListUMStateManager.isInSearchStateFlow
|
||||
.collectLatest { isInSearchMode ->
|
||||
activeListManager = if (isInSearchMode) {
|
||||
searchMarketsListManager
|
||||
} else {
|
||||
searchMarketsListManager.clearStateAndStopAllActions()
|
||||
mainMarketsListManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
marketsListUMStateManager.searchQueryFlow
|
||||
.filter { it.isNotEmpty() }
|
||||
.debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS)
|
||||
.filter { activeListManager == searchMarketsListManager }
|
||||
.collectLatest {
|
||||
searchMarketsListManager.reload(searchText = it)
|
||||
}
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
searchMarketsListManager
|
||||
.onLastBatchLoadedSuccess
|
||||
.collectLatest {
|
||||
searchMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval)
|
||||
modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS)
|
||||
}
|
||||
}
|
||||
|
||||
// initial loading
|
||||
marketsListManager.reload(
|
||||
interval = marketsListUMStateManager.selectedInterval,
|
||||
sortBy = marketsListUMStateManager.selectedSortByType,
|
||||
)
|
||||
mainMarketsListManager.reload()
|
||||
}
|
||||
|
||||
private var updateQuotesJob = JobHolder()
|
||||
|
|
@ -167,7 +217,7 @@ internal class MarketsListModel @Inject constructor(
|
|||
delay(timeMillis)
|
||||
// Update quotes only when the container bottom sheet is in the expanded state
|
||||
containerBottomSheetState.first { it == BottomSheetState.EXPANDED }
|
||||
activeListManager.updateQuotes()
|
||||
activeListManager.updateQuotes() // TODO update a batch that is currently on screen
|
||||
}
|
||||
}.saveIn(updateQuotesJob)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ internal class MarketsTokenItemConverter(
|
|||
trendPercentText = value.getTrendPercent(),
|
||||
trendType = value.getTrendType(),
|
||||
chardData = value.getChartData(),
|
||||
showUnder100kMarketCap = value.isUnder100kMarketCap(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ internal class MarketsTokenItemConverter(
|
|||
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
|
||||
val prevPrice = prev?.tokenQuotes?.currentPrice
|
||||
|
||||
val priceText = BigDecimalFormatter.formatFiatAmount(
|
||||
val priceText = BigDecimalFormatter.formatFiatAmountUncapped(
|
||||
fiatAmount = tokenQuotes.currentPrice,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
|
|
@ -141,4 +142,12 @@ internal class MarketsTokenItemConverter(
|
|||
useAbsoluteValue = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarket.isUnder100kMarketCap(): Boolean {
|
||||
return tokenQuotes.currentPrice.compareTo(decimal100k) == -1
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val decimal100k: BigDecimal = BigDecimal.valueOf(100_000)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.markets.*
|
||||
import com.tangem.features.markets.model.converters.MarketsTokenItemConverter
|
||||
import com.tangem.features.markets.model.utils.logAction
|
||||
import com.tangem.features.markets.model.utils.logStatus
|
||||
import com.tangem.features.markets.model.utils.logUpdateResults
|
||||
import com.tangem.features.markets.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.ui.entity.MarketsListUM.TrendInterval
|
||||
|
|
@ -18,23 +19,27 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val LOG_EVENTS = false
|
||||
private const val LOG_EVENTS = true
|
||||
|
||||
internal class MarketsListUiItemsManager(
|
||||
private val logTag: String = "main",
|
||||
@Suppress("LongParameterList")
|
||||
internal class MarketsListBatchFlowManager(
|
||||
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
|
||||
private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType,
|
||||
private val currentTrendInterval: Provider<TrendInterval>,
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val currentSearchText: Provider<String?>,
|
||||
private val currentSortByType: Provider<SortByTypeUM>,
|
||||
private val modelScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val actionsFlow = MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>()
|
||||
|
||||
private val batchFlow = getMarketsTokenListFlowUseCase(
|
||||
TokenListBatchingContext(
|
||||
batchingContext = TokenListBatchingContext(
|
||||
actionsFlow = actionsFlow,
|
||||
coroutineScope = modelScope,
|
||||
),
|
||||
batchFlowType = batchFlowType,
|
||||
)
|
||||
|
||||
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>>
|
||||
|
|
@ -53,7 +58,7 @@ internal class MarketsListUiItemsManager(
|
|||
)
|
||||
|
||||
val onLastBatchLoadedSuccess = batchFlow.state
|
||||
.distinctUntilChanged { old, new -> old.status === new.status }
|
||||
.distinctUntilChanged { old, new -> old.status == new.status && old.data.size == new.data.size }
|
||||
.mapNotNull {
|
||||
when (val status = it.status) {
|
||||
is PaginationStatus.Paginating -> {
|
||||
|
|
@ -70,6 +75,28 @@ internal class MarketsListUiItemsManager(
|
|||
}
|
||||
}
|
||||
|
||||
val isInInitialLoadingErrorState = batchFlow.state
|
||||
.map { it.status is PaginationStatus.InitialLoadingError }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val isSearchNotFoundState = batchFlow.state
|
||||
.map {
|
||||
currentSearchText().isNullOrEmpty().not() &&
|
||||
it.status is PaginationStatus.EndOfPagination &&
|
||||
it.data.isEmpty()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
private val uiBatches = MutableStateFlow<List<Batch<Int, List<MarketsListItemUM>>>>(emptyList())
|
||||
|
||||
init {
|
||||
|
|
@ -86,11 +113,16 @@ internal class MarketsListUiItemsManager(
|
|||
|
||||
if (LOG_EVENTS) {
|
||||
batchFlow.updateResults
|
||||
.onEach { logUpdateResults(logTag, it) }
|
||||
.onEach { logUpdateResults(batchFlowType.name, it) }
|
||||
.launchIn(modelScope)
|
||||
|
||||
batchFlow.state
|
||||
.map { it.status }
|
||||
.onEach { logStatus(batchFlowType.name, it) }
|
||||
.launchIn(modelScope)
|
||||
|
||||
actionsFlow
|
||||
.onEach { logAction(logTag, it) }
|
||||
.onEach { logAction(batchFlowType.name, it) }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
}
|
||||
|
|
@ -144,17 +176,21 @@ internal class MarketsListUiItemsManager(
|
|||
}
|
||||
}
|
||||
|
||||
fun reload(interval: TrendInterval, sortBy: SortByTypeUM) {
|
||||
fun reload(searchText: String? = null) {
|
||||
modelScope.launch {
|
||||
uiBatches.value = emptyList()
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TokenMarketListConfig(
|
||||
fiatPriceCurrency = currentAppCurrency().code,
|
||||
searchText = null,
|
||||
showUnder100kMarketCapTokens = false,
|
||||
priceChangeInterval = interval.toBatchRequestInterval(),
|
||||
order = sortBy.toRequestOrder(),
|
||||
searchText = if (currentSearchText() == null) {
|
||||
null
|
||||
} else {
|
||||
searchText ?: currentSearchText()
|
||||
},
|
||||
showUnder100kMarketCapTokens = false, // TODO
|
||||
priceChangeInterval = currentTrendInterval().toBatchRequestInterval(),
|
||||
order = currentSortByType().toRequestOrder(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -229,6 +265,13 @@ internal class MarketsListUiItemsManager(
|
|||
}
|
||||
}
|
||||
|
||||
fun clearStateAndStopAllActions() {
|
||||
uiBatches.value = emptyList()
|
||||
modelScope.launch {
|
||||
actionsFlow.emit(BatchAction.Reset)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBatchKeysByItemIds(ids: List<String>): Set<Int> {
|
||||
val currentData = batchFlow.state.value.data
|
||||
|
||||
|
|
@ -3,32 +3,43 @@ package com.tangem.features.markets.model.statemanager
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.model.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.ui.entity.ListUM
|
||||
import com.tangem.features.markets.ui.entity.MarketsListItemUM
|
||||
import com.tangem.features.markets.ui.entity.MarketsListUM
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Stable
|
||||
internal class MarketsListUMStateManager(
|
||||
private val onLoadMoreUiItems: () -> Unit,
|
||||
private val visibleItemsChanged: (itemsKeys: List<String>) -> Unit,
|
||||
private val onRetryButtonClicked: () -> Unit,
|
||||
) {
|
||||
|
||||
private var sortByBottomSheetIsShown
|
||||
get() = state.value.sortByBottomSheet.isShow
|
||||
set(value) = state.update { it.copy(sortByBottomSheet = it.sortByBottomSheet.copy(isShow = value)) }
|
||||
|
||||
private val isInSearchStateFlow = MutableStateFlow(false)
|
||||
var searchQuery
|
||||
get() = state.value.searchBar.query
|
||||
private set(value) = state.update {
|
||||
it.copy(
|
||||
searchBar = it.searchBar.copy(
|
||||
query = value,
|
||||
isActive = value.isNotEmpty(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
var isInSearchState
|
||||
get() = isInSearchStateFlow.value
|
||||
set(value) { isInSearchStateFlow.value = value }
|
||||
get() = state.value.searchBar.isActive
|
||||
private set(value) = state.update { it.copy(searchBar = it.searchBar.copy(isActive = value)) }
|
||||
|
||||
var selectedSortByType
|
||||
get() = state.value.selectedSortBy
|
||||
|
|
@ -40,29 +51,64 @@ internal class MarketsListUMStateManager(
|
|||
selectedOption = value,
|
||||
),
|
||||
),
|
||||
list = if (it.list is ListUM.Content && it.selectedSortBy != value) {
|
||||
it.list.copy(triggerScrollReset = triggeredEvent(Unit) { consumeTriggerResetScrollEvent() })
|
||||
} else {
|
||||
it.list
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
var selectedInterval
|
||||
get() = state.value.selectedInterval
|
||||
set(value) = state.update { it.copy(selectedInterval = value) }
|
||||
set(value) = state.update {
|
||||
it.copy(
|
||||
selectedInterval = value,
|
||||
list = if (it.list is ListUM.Content &&
|
||||
it.selectedSortBy != SortByTypeUM.Rating &&
|
||||
it.selectedInterval != value
|
||||
) {
|
||||
it.list.copy(triggerScrollReset = triggeredEvent(Unit) { consumeTriggerResetScrollEvent() })
|
||||
} else {
|
||||
it.list
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val state = MutableStateFlow(state())
|
||||
val isInSearchStateFlow = state.map { it.searchBar.isActive }.distinctUntilChanged()
|
||||
val searchQueryFlow = state.map { it.searchBar.query }.distinctUntilChanged()
|
||||
|
||||
fun onUiItemsChanged(uiItems: ImmutableList<MarketsListItemUM>) {
|
||||
fun onUiItemsChanged(
|
||||
isInErrorState: Boolean,
|
||||
isSearchNotFound: Boolean,
|
||||
uiItems: ImmutableList<MarketsListItemUM>,
|
||||
) {
|
||||
state.update {
|
||||
if (uiItems.isEmpty()) {
|
||||
it.copy(
|
||||
list = ListUM.Loading,
|
||||
)
|
||||
} else {
|
||||
it.copy(
|
||||
list = ListUM.Content(
|
||||
items = uiItems,
|
||||
loadMore = onLoadMoreUiItems,
|
||||
visibleIdsChanged = visibleItemsChanged,
|
||||
),
|
||||
)
|
||||
when {
|
||||
isInErrorState -> {
|
||||
it.copy(
|
||||
list = ListUM.LoadingError(onRetryClicked = onRetryButtonClicked),
|
||||
)
|
||||
}
|
||||
isSearchNotFound -> {
|
||||
it.copy(list = ListUM.SearchNothingFound)
|
||||
}
|
||||
uiItems.isEmpty() -> {
|
||||
it.copy(list = ListUM.Loading)
|
||||
}
|
||||
else -> {
|
||||
it.copy(
|
||||
list = ListUM.Content(
|
||||
items = uiItems,
|
||||
loadMore = onLoadMoreUiItems,
|
||||
visibleIdsChanged = visibleItemsChanged,
|
||||
showUnder100kTokens = true,
|
||||
onShowTokensUnder100kClicked = { },
|
||||
triggerScrollReset = consumedEvent(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -71,10 +117,10 @@ internal class MarketsListUMStateManager(
|
|||
list = ListUM.Loading,
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
query = "", // TODO
|
||||
onQueryChange = {}, // TODO
|
||||
isActive = false, // TODO
|
||||
onActiveChange = { }, // TODO
|
||||
query = "",
|
||||
onQueryChange = { searchQuery = it },
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
selectedSortBy = SortByTypeUM.Rating,
|
||||
selectedInterval = MarketsListUM.TrendInterval.H24,
|
||||
|
|
@ -103,4 +149,18 @@ internal class MarketsListUMStateManager(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun consumeTriggerResetScrollEvent() {
|
||||
state.update {
|
||||
it.copy(
|
||||
list = if (it.list is ListUM.Content) {
|
||||
it.list.copy(
|
||||
triggerScrollReset = consumedEvent(),
|
||||
)
|
||||
} else {
|
||||
it.list
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,18 @@ import com.tangem.domain.markets.TokenMarketListConfig
|
|||
import com.tangem.domain.markets.TokenMarketUpdateRequest
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchUpdateResult
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import timber.log.Timber
|
||||
|
||||
internal fun logStatus(tag: String, status: PaginationStatus<List<TokenMarket>>) {
|
||||
Timber.tag(tag).d(
|
||||
"""
|
||||
Status
|
||||
$status
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun logAction(tag: String, action: BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>) {
|
||||
when (action) {
|
||||
is BatchAction.Reload -> Timber.tag(tag).d(
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
package com.tangem.features.markets.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
|
|
@ -28,31 +28,41 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
|||
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.component.BottomSheetState
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.model.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.ui.components.MarketsListItem
|
||||
import com.tangem.features.markets.ui.components.MarketsListItemPlaceholder
|
||||
import com.tangem.features.markets.ui.components.MarketsListLazyColumn
|
||||
import com.tangem.features.markets.ui.components.MarketsListSortByBottomSheet
|
||||
import com.tangem.features.markets.ui.entity.ListUM
|
||||
import com.tangem.features.markets.ui.entity.MarketsListUM
|
||||
import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun MarketsList(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
|
||||
internal fun MarketsList(
|
||||
state: MarketsListUM,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
bottomSheetState: BottomSheetState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Content(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
)
|
||||
|
||||
MarketsListSortByBottomSheet(config = state.sortByBottomSheet)
|
||||
KeyboardEvents(
|
||||
isSortByBottomSheetShown = state.sortByBottomSheet.isShow,
|
||||
bottomSheetState = bottomSheetState,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -66,7 +76,6 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
|
|||
.background(color = TangemTheme.colors.background.primary),
|
||||
) {
|
||||
SearchBar(
|
||||
state = state.searchBar,
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.padding(
|
||||
|
|
@ -77,28 +86,40 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi
|
|||
.onGloballyPositioned {
|
||||
with(density) { onHeaderSizeChange(it.size.height.toDp()) }
|
||||
},
|
||||
state = state.searchBar,
|
||||
)
|
||||
Spacer(Modifier.height(TangemTheme.dimens.spacing20))
|
||||
Column(Modifier.padding(horizontal = TangemTheme.dimens.size16)) {
|
||||
Title()
|
||||
SpacerH12()
|
||||
Options(
|
||||
sortByTypeUM = state.selectedSortBy,
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onIntervalClick,
|
||||
onSortByClick = state.onSortByButtonClick,
|
||||
)
|
||||
Title(isInSearchMode = state.isInSearchMode)
|
||||
AnimatedVisibility(state.isInSearchMode.not()) {
|
||||
Column {
|
||||
SpacerH12()
|
||||
Options(
|
||||
sortByTypeUM = state.selectedSortBy,
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onIntervalClick,
|
||||
onSortByClick = state.onSortByButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
SpacerH12()
|
||||
Items(state = state.list)
|
||||
ItemsList(
|
||||
isInSearchMode = state.isInSearchMode,
|
||||
state = state.list,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(modifier: Modifier = Modifier) {
|
||||
private fun Title(isInSearchMode: Boolean, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = stringResource(id = R.string.markets_common_title),
|
||||
text = if (isInSearchMode) {
|
||||
stringResource(id = R.string.markets_search_result_title)
|
||||
} else {
|
||||
stringResource(id = R.string.markets_common_title)
|
||||
},
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
|
@ -158,105 +179,49 @@ private fun Options(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Items(state: ListUM, modifier: Modifier = Modifier) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val scrollEnabled = state !is ListUM.Loading
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
private fun ItemsList(isInSearchMode: Boolean, state: ListUM, modifier: Modifier = Modifier) {
|
||||
val searchLazyListState = rememberLazyListState()
|
||||
val mainLazyListState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier.nestedScroll(DisableParentConnection),
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
userScrollEnabled = scrollEnabled,
|
||||
) {
|
||||
// ATTENTION! There should be no elements with a string key value except MarketsListItem!
|
||||
when (state) {
|
||||
ListUM.Loading -> {
|
||||
items(count = 50, key = { it }) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
ListUM.SearchNothingFound -> {
|
||||
// TODO
|
||||
}
|
||||
is ListUM.Content -> {
|
||||
items(
|
||||
items = state.items,
|
||||
key = { it.id },
|
||||
) { item ->
|
||||
MarketsListItem(
|
||||
model = item,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
if (state is ListUM.Loading) {
|
||||
lazyListState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
VisibleItemsTracker(lazyListState, state)
|
||||
|
||||
InfiniteListHandler(
|
||||
listState = lazyListState,
|
||||
buffer = 50,
|
||||
onLoadMore = remember(state) {
|
||||
{
|
||||
if (state is ListUM.Content) {
|
||||
state.loadMore()
|
||||
}
|
||||
}
|
||||
MarketsListLazyColumn(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
isInSearchMode = isInSearchMode,
|
||||
lazyListState = if (isInSearchMode) {
|
||||
searchLazyListState
|
||||
} else {
|
||||
mainLazyListState
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VisibleItemsTracker(listState: LazyListState, state: ListUM) {
|
||||
val visibleItems by remember {
|
||||
derivedStateOf {
|
||||
listState.layoutInfo.visibleItemsInfo.mapNotNull { it.key as? String }
|
||||
private fun KeyboardEvents(isSortByBottomSheetShown: Boolean, bottomSheetState: BottomSheetState) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val keyboard by keyboardAsState()
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
BackHandler(enabled = keyboard is Keyboard.Opened) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
|
||||
LaunchedEffect(keyboard) {
|
||||
if (keyboard is Keyboard.Closed) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(listState.isScrollInProgress, visibleItems) {
|
||||
if (state is ListUM.Content && listState.isScrollInProgress.not()) {
|
||||
state.visibleIdsChanged(visibleItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Unit, buffer: Int = 2) {
|
||||
val loadMore by remember {
|
||||
derivedStateOf {
|
||||
val layoutInfo = listState.layoutInfo
|
||||
val totalItemsNumber = layoutInfo.totalItemsCount
|
||||
val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1
|
||||
|
||||
lastVisibleItemIndex > totalItemsNumber - buffer
|
||||
}
|
||||
LaunchedEffect(isSortByBottomSheetShown) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
|
||||
val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } }
|
||||
var emitted by remember(totalItemsCount) { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(loadMore) {
|
||||
if (loadMore && !emitted) {
|
||||
emitted = true
|
||||
onLoadMore()
|
||||
LaunchedEffect(bottomSheetState) {
|
||||
if (bottomSheetState == BottomSheetState.COLLAPSED) {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object DisableParentConnection : NestedScrollConnection {
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||
return available.copy(x = 0f)
|
||||
}
|
||||
}
|
||||
|
||||
//region: Preview
|
||||
|
||||
@Preview
|
||||
|
|
@ -272,8 +237,11 @@ private fun Preview() {
|
|||
item.copy(id = index.toString())
|
||||
}
|
||||
.toImmutableList(),
|
||||
showUnder100kTokens = false,
|
||||
loadMore = {},
|
||||
visibleIdsChanged = {},
|
||||
onShowTokensUnder100kClicked = {},
|
||||
triggerScrollReset = consumedEvent(),
|
||||
),
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
|
||||
|
|
@ -287,12 +255,13 @@ private fun Preview() {
|
|||
onIntervalClick = {},
|
||||
onSortByButtonClick = {},
|
||||
sortByBottomSheet = TangemBottomSheetConfig(
|
||||
false,
|
||||
isShow = false,
|
||||
onDismissRequest = {},
|
||||
content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {},
|
||||
),
|
||||
),
|
||||
onHeaderSizeChange = {},
|
||||
bottomSheetState = BottomSheetState.EXPANDED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
package com.tangem.features.markets.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.ui.entity.ListUM
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
internal fun MarketsListLazyColumn(
|
||||
state: ListUM,
|
||||
isInSearchMode: Boolean,
|
||||
lazyListState: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
SideEffect {
|
||||
if (state is ListUM.Loading) {
|
||||
coroutineScope.launch {
|
||||
lazyListState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state is ListUM.Content) {
|
||||
EventEffect(state.triggerScrollReset) {
|
||||
lazyListState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
if (state is ListUM.Loading) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.nestedScroll(DisableParentConnection),
|
||||
state = rememberLazyListState(),
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
userScrollEnabled = false,
|
||||
) {
|
||||
items(count = 100, key = { it }) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = modifier.nestedScroll(DisableParentConnection),
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight),
|
||||
userScrollEnabled = true,
|
||||
) {
|
||||
// ATTENTION! There should be no elements with a string key value except MarketsListItem!
|
||||
when (state) {
|
||||
is ListUM.LoadingError -> {
|
||||
item(key = "loading error".hashCode()) {
|
||||
LoadingErrorItem(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
onTryAgain = state.onRetryClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
ListUM.SearchNothingFound -> {
|
||||
item(key = "not found text".hashCode()) {
|
||||
SearchNothingFoundText(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
is ListUM.Content -> {
|
||||
items(
|
||||
items = state.items,
|
||||
key = { it.id },
|
||||
) { item ->
|
||||
MarketsListItem(model = item)
|
||||
}
|
||||
|
||||
if (isInSearchMode && state.showUnder100kTokens.not()) {
|
||||
item(key = "show tokens under 100k".hashCode()) {
|
||||
ShowTokensUnder100kItem(
|
||||
onShowTokensClick = state.onShowTokensUnder100kClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VisibleItemsTracker(lazyListState, state)
|
||||
|
||||
InfiniteListHandler(
|
||||
listState = lazyListState,
|
||||
buffer = LOAD_NEXT_PAGE_ON_END_INDEX,
|
||||
onLoadMore = remember(state) {
|
||||
{
|
||||
if (state is ListUM.Content) {
|
||||
state.loadMore()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = onTryAgain)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.markets_search_see_tokens_under_100k),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.markets_search_show_tokens),
|
||||
onClick = onShowTokensClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchNothingFoundText(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.markets_search_token_no_result_title),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) {
|
||||
val visibleItems by remember {
|
||||
derivedStateOf {
|
||||
listState.layoutInfo.visibleItemsInfo.mapNotNull { it.key as? String }
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(listState.isScrollInProgress, visibleItems) {
|
||||
if (state is ListUM.Content && listState.isScrollInProgress.not()) {
|
||||
state.visibleIdsChanged(visibleItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Unit, buffer: Int = 2) {
|
||||
val loadMore by remember {
|
||||
derivedStateOf {
|
||||
val layoutInfo = listState.layoutInfo
|
||||
val totalItemsNumber = layoutInfo.totalItemsCount
|
||||
val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1
|
||||
|
||||
lastVisibleItemIndex > totalItemsNumber - buffer
|
||||
}
|
||||
}
|
||||
|
||||
val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } }
|
||||
var emitted by remember(totalItemsCount) { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(loadMore) {
|
||||
if (loadMore && !emitted) {
|
||||
emitted = true
|
||||
onLoadMore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object DisableParentConnection : NestedScrollConnection {
|
||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||
return available.copy(x = 0f)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.model.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.features.markets.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.markets_loading_error_title),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.try_to_load_data_again_button_title),
|
||||
onClick = onRetryClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
UnableToLoadData(onRetryClick = {})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
package com.tangem.features.markets.ui.entity
|
||||
|
||||
sealed class ListItem
|
||||
|
|
@ -17,6 +17,7 @@ data class MarketsListItemUM(
|
|||
val trendPercentText: String,
|
||||
val trendType: PriceChangeType,
|
||||
val chardData: MarketChartRawData?,
|
||||
val showUnder100kMarketCap: Boolean = false,
|
||||
) {
|
||||
val chartType: MarketChartLook.Type = when (trendType) {
|
||||
PriceChangeType.UP,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.markets.ui.entity
|
|||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
|
@ -17,6 +18,9 @@ internal data class MarketsListUM(
|
|||
val onIntervalClick: (TrendInterval) -> Unit,
|
||||
val onSortByButtonClick: () -> Unit,
|
||||
) {
|
||||
val isInSearchMode
|
||||
get() = searchBar.isActive
|
||||
|
||||
enum class TrendInterval(val text: TextReference) {
|
||||
H24(resourceReference(R.string.markets_selector_interval_24h_title)),
|
||||
D7(resourceReference(R.string.markets_selector_interval_7d_title)),
|
||||
|
|
@ -37,10 +41,18 @@ sealed class ListUM {
|
|||
|
||||
data class Content(
|
||||
val items: ImmutableList<MarketsListItemUM>,
|
||||
val showUnder100kTokens: Boolean,
|
||||
val loadMore: () -> Unit,
|
||||
val visibleIdsChanged: (List<String>) -> Unit,
|
||||
val onShowTokensUnder100kClicked: () -> Unit,
|
||||
val triggerScrollReset: StateEvent<Unit>,
|
||||
) : ListUM()
|
||||
|
||||
data object Loading : ListUM()
|
||||
|
||||
data class LoadingError(
|
||||
val onRetryClicked: () -> Unit,
|
||||
) : ListUM()
|
||||
|
||||
data object SearchNothingFound : ListUM()
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.features.markets.model
|
||||
package com.tangem.features.markets.ui.entity
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.features.markets.ui.entity.SortByTypeUM
|
||||
|
||||
data class SortByBottomSheetContentUM(
|
||||
val selectedOption: SortByTypeUM,
|
||||
Loading…
Add table
Add a link
Reference in a new issue