Updated on 2026-08-14
This commit is contained in:
parent
b0ac045d0d
commit
626d0914a0
31 changed files with 1201 additions and 95 deletions
|
|
@ -10,7 +10,7 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
internal interface FeedComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package com.tangem.features.feed.model.converter
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.*
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class MarketsTokenItemConverter(
|
||||
private val currentTrendInterval: MarketsListUM.TrendInterval,
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<TokenMarket, MarketsListItemUM> {
|
||||
|
||||
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = false)
|
||||
|
||||
override fun convert(value: TokenMarket): MarketsListItemUM {
|
||||
return MarketsListItemUM(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
currencySymbol = value.symbol,
|
||||
ratingPosition = value.marketRating?.toString(),
|
||||
marketCap = value.getMarketCap(),
|
||||
iconUrl = value.imageUrlLarge,
|
||||
price = value.getCurrentPrice(),
|
||||
trendPercentText = value.getTrendPercent(),
|
||||
trendType = value.getTrendType(),
|
||||
chartData = value.getChartData(),
|
||||
isUnder100kMarketCap = value.isUnderMarketCapLimit,
|
||||
stakingRate = value.stakingRate?.format { percent() }?.let {
|
||||
resourceReference(R.string.markets_apy_placeholder, wrappedList(it))
|
||||
},
|
||||
updateTimestamp = value.updateTimestamp,
|
||||
)
|
||||
}
|
||||
|
||||
fun update(prev: TokenMarket, prevUI: MarketsListItemUM, new: TokenMarket): MarketsListItemUM {
|
||||
require(prev.id == new.id) {
|
||||
"Ids is not the same during update TokenMarket item: previousItem[${prev.id}] != newItem[${new.id}]"
|
||||
}
|
||||
|
||||
return prevUI.copy(
|
||||
name = new.name,
|
||||
currencySymbol = new.symbol,
|
||||
ratingPosition = new.marketRating?.toString(),
|
||||
marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() },
|
||||
iconUrl = new.imageUrlLarge,
|
||||
price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) {
|
||||
new.getCurrentPrice(
|
||||
prev = prev,
|
||||
)
|
||||
},
|
||||
trendPercentText = ifChanged(
|
||||
prev.tokenQuotesShort,
|
||||
new.tokenQuotesShort,
|
||||
prevUI.trendPercentText,
|
||||
) { new.getTrendPercent() },
|
||||
trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() },
|
||||
chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() },
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <T, R> ifChanged(prev: T, new: T, prevR: R, force: Boolean = false, change: (T) -> R): R {
|
||||
return if (force || prev != new) change(new) else prevR
|
||||
}
|
||||
|
||||
private fun TokenMarket.getMarketCap(): String? {
|
||||
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
|
||||
|
||||
return value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).compact(
|
||||
threeDigitsMethod = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
|
||||
val prevPrice = prev?.tokenQuotesShort?.currentPrice
|
||||
|
||||
val priceText = tokenQuotesShort.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).price()
|
||||
}
|
||||
|
||||
val changeType = if (prevPrice != null) {
|
||||
if (tokenQuotesShort.currentPrice > prevPrice) {
|
||||
PriceChangeType.UP
|
||||
} else {
|
||||
PriceChangeType.DOWN
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return MarketsListItemUM.Price(
|
||||
text = priceText,
|
||||
changeType = changeType,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarket.getChartData(): MarketChartRawData? {
|
||||
val chart = when (currentTrendInterval) {
|
||||
MarketsListUM.TrendInterval.H24 -> tokenCharts.h24
|
||||
MarketsListUM.TrendInterval.D7 -> tokenCharts.week
|
||||
MarketsListUM.TrendInterval.M1 -> tokenCharts.month
|
||||
}
|
||||
|
||||
return chart?.let { ct ->
|
||||
priceAndTimePointValuesConverter.convert(
|
||||
MarketChartData.Data(
|
||||
y = ct.priceY.toImmutableList(),
|
||||
x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
).sorted(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun TokenMarket.getTrendType(): PriceChangeType {
|
||||
val percent = when (currentTrendInterval) {
|
||||
MarketsListUM.TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent
|
||||
MarketsListUM.TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
|
||||
MarketsListUM.TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
|
||||
}
|
||||
val scaled = percent?.setScale(4, RoundingMode.HALF_UP)
|
||||
return when {
|
||||
scaled == null -> PriceChangeType.NEUTRAL
|
||||
scaled > BigDecimal.ZERO -> PriceChangeType.UP
|
||||
scaled < BigDecimal.ZERO -> PriceChangeType.DOWN
|
||||
else -> PriceChangeType.NEUTRAL
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getTrendPercent(): String {
|
||||
val percent = when (currentTrendInterval) {
|
||||
MarketsListUM.TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent
|
||||
MarketsListUM.TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
|
||||
MarketsListUM.TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
|
||||
}
|
||||
|
||||
return percent.format { percent() }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
package com.tangem.features.feed.model.feed
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.GetTopFiveMarketTokenUseCase
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase
|
||||
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
|
||||
import com.tangem.features.feed.impl.R
|
||||
|
|
@ -13,10 +18,12 @@ import com.tangem.features.feed.ui.feed.state.*
|
|||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentHashMap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
|
|
@ -28,11 +35,30 @@ internal class FeedComponentModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase,
|
||||
private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase,
|
||||
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val _state = MutableStateFlow(initialState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private var quotesUpdateJob: Job? = null
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
|
||||
private val marketsBatchFlowManager = FeedMarketsBatchFlowManager(
|
||||
getTopFiveMarketTokenUseCase = getTopFiveMarketTokenUseCase,
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
private val searchBarStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SearchBarStateFactory(
|
||||
currentStateProvider = Provider { _state.value },
|
||||
|
|
@ -55,8 +81,45 @@ internal class FeedComponentModel @Inject constructor(
|
|||
_state.update { feedListUM ->
|
||||
feedListUM.copy(
|
||||
searchBar = _state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange),
|
||||
feedListCallbacks = feedListUM.feedListCallbacks.copy(
|
||||
onSortTypeClick = ::onSortTypeClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
combine(
|
||||
marketsBatchFlowManager.itemsByOrder,
|
||||
marketsBatchFlowManager.loadingStatesByOrder,
|
||||
marketsBatchFlowManager.errorStatesByOrder,
|
||||
) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder ->
|
||||
updateMarketCharts(itemsByOrder, loadingStatesByOrder, errorStatesByOrder)
|
||||
val currentSortType = _state.value.marketChartConfig.currentSortByType
|
||||
val items = itemsByOrder[currentSortType]
|
||||
val isLoading = loadingStatesByOrder[currentSortType] == true
|
||||
if (items != null && items.isNotEmpty() && !isLoading) {
|
||||
val order = currentSortType.toOrder()
|
||||
marketsBatchFlowManager.loadCharts(order)
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
currentAppCurrency.drop(1).collect {
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
}
|
||||
}
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
TokenMarketListConfig.Order.entries.forEach { order ->
|
||||
marketsBatchFlowManager.getOnLastBatchLoadedSuccessFlow(order)?.collect { batchKey ->
|
||||
marketsBatchFlowManager.loadCharts(order)
|
||||
if (batchKey == 0) {
|
||||
startQuotesUpdateTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun subscribeOnTrendingNews() {
|
||||
|
|
@ -88,10 +151,10 @@ internal class FeedComponentModel @Inject constructor(
|
|||
marketChartConfig = MarketChartConfig(
|
||||
marketCharts = buildMap {
|
||||
SortByTypeUM.entries.forEach {
|
||||
put(it, MarketChartUM.Loading)
|
||||
} // TODO in [REDACTED_TASK_KEY] add correct sorting
|
||||
put(it, MarketChartUM.LoadingError(onRetryClicked = marketsBatchFlowManager::reloadAll))
|
||||
}
|
||||
}.toPersistentHashMap(),
|
||||
currentSortByType = SortByTypeUM.TopGainers,
|
||||
currentSortByType = SortByTypeUM.Trending,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -100,4 +163,116 @@ internal class FeedComponentModel @Inject constructor(
|
|||
val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault())
|
||||
return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate)
|
||||
}
|
||||
|
||||
private fun updateMarketCharts(
|
||||
itemsByOrder: Map<SortByTypeUM, ImmutableList<com.tangem.common.ui.markets.models.MarketsListItemUM>>,
|
||||
loadingStatesByOrder: Map<SortByTypeUM, Boolean>,
|
||||
errorStatesByOrder: Map<SortByTypeUM, Boolean>,
|
||||
) {
|
||||
_state.update { currentState ->
|
||||
val newMarketCharts = buildMap {
|
||||
SortByTypeUM.entries.forEach { sortByType ->
|
||||
val items = itemsByOrder[sortByType] ?: persistentListOf()
|
||||
val isLoading = loadingStatesByOrder[sortByType] == true
|
||||
val hasError = errorStatesByOrder[sortByType] == true
|
||||
|
||||
when {
|
||||
hasError -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.LoadingError(
|
||||
onRetryClicked = {
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
isLoading -> {
|
||||
put(sortByType, MarketChartUM.Loading)
|
||||
}
|
||||
items.isEmpty() -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.LoadingError(
|
||||
onRetryClicked = {
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.Content(
|
||||
items = items,
|
||||
sortChartConfig = SortChartConfigUM(
|
||||
sortByType = sortByType,
|
||||
isSelected = sortByType == currentState.marketChartConfig.currentSortByType,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toPersistentHashMap()
|
||||
|
||||
currentState.copy(
|
||||
marketChartConfig = currentState.marketChartConfig.copy(
|
||||
marketCharts = newMarketCharts,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSortTypeClick(sortByType: SortByTypeUM) {
|
||||
_state.update { currentState ->
|
||||
val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) ->
|
||||
when (chart) {
|
||||
is MarketChartUM.Content -> {
|
||||
chart.copy(
|
||||
sortChartConfig = chart.sortChartConfig.copy(
|
||||
isSelected = chartSortType == sortByType,
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> chart
|
||||
}
|
||||
}
|
||||
|
||||
currentState.copy(
|
||||
marketChartConfig = currentState.marketChartConfig.copy(
|
||||
currentSortByType = sortByType,
|
||||
marketCharts = updatedCharts.toPersistentHashMap(),
|
||||
),
|
||||
)
|
||||
}
|
||||
modelScope.launch(dispatchers.default) {
|
||||
marketsBatchFlowManager.loadCharts(sortByType.toOrder())
|
||||
}
|
||||
}
|
||||
|
||||
private fun startQuotesUpdateTimer() {
|
||||
quotesUpdateJob?.cancel()
|
||||
quotesUpdateJob = modelScope.launch {
|
||||
while (true) {
|
||||
delay(DELAY_TO_FETCH_QUOTES)
|
||||
marketsBatchFlowManager.updateQuotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order {
|
||||
return when (this) {
|
||||
SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating
|
||||
SortByTypeUM.Trending -> TokenMarketListConfig.Order.Trending
|
||||
SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers
|
||||
SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers
|
||||
SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers
|
||||
SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DELAY_TO_FETCH_QUOTES = 60_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -29,6 +31,10 @@ import androidx.compose.ui.text.withStyle
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.markets.MarketChartLoadingError
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -49,9 +55,6 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.components.MarketsListItem
|
||||
import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
|
||||
@Composable
|
||||
|
|
@ -101,10 +104,12 @@ internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
|||
|
||||
SpacerH(32.dp)
|
||||
|
||||
MarketBlock(
|
||||
marketChartConfig = state.marketChartConfig,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
)
|
||||
state.marketChartConfig.marketCharts[SortByTypeUM.Rating]?.let { marketChartUM ->
|
||||
MarketBlock(
|
||||
marketChart = marketChartUM,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
)
|
||||
}
|
||||
|
||||
NewsBlock(
|
||||
news = state.news,
|
||||
|
|
@ -120,44 +125,43 @@ internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
marketChartConfig.marketCharts[SortByTypeUM.Rating]?.let { chart ->
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
marketChart = chart,
|
||||
private fun MarketBlock(marketChart: MarketChartUM, feedListCallbacks: FeedListCallbacks) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
marketChart = marketChart,
|
||||
)
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
val onSeeAllClick by rememberUpdatedState {
|
||||
feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType)
|
||||
}
|
||||
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
text = stringResourceSafe(R.string.markets_pulse_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) },
|
||||
onSeeAllClick = { onSeeAllClick() },
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
|
|
@ -330,11 +334,7 @@ private fun Charts(
|
|||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
when (marketChart) {
|
||||
MarketChartUM.Loading -> {
|
||||
repeat(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
|
|
@ -342,7 +342,10 @@ private fun Charts(
|
|||
}
|
||||
}
|
||||
is MarketChartUM.LoadingError -> {
|
||||
// TODO will be created in [REDACTED_TASK_KEY]
|
||||
MarketChartLoadingError(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onRetryClick = marketChart.onRetryClicked,
|
||||
)
|
||||
}
|
||||
is MarketChartUM.Content -> {
|
||||
marketChart.items.fastForEach { chart ->
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.common.ui.news.DefaultLoadingArticle
|
||||
import com.tangem.common.ui.news.TrendingLoadingArticle
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder
|
||||
|
||||
@Composable
|
||||
internal fun MarketLoadingBlock() {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
package com.tangem.features.feed.ui.feed.preview
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.*
|
||||
|
||||
|
|
@ -94,7 +93,6 @@ internal object FeedListPreviewDataProvider {
|
|||
): MarketChartUM.Content {
|
||||
return MarketChartUM.Content(
|
||||
items = items,
|
||||
triggerScrollReset = consumedEvent(),
|
||||
sortChartConfig = SortChartConfigUM(
|
||||
sortByType = sortByType,
|
||||
isSelected = isSelected,
|
||||
|
|
@ -107,7 +105,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 1,
|
||||
title = "Bitcoin ETF reaches new highs, institutions pile in",
|
||||
score = 0.82f,
|
||||
createdAt = "2h ago",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = true,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -116,7 +114,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 2,
|
||||
title = "Layer 2 networks battle for dominance amid fee wars",
|
||||
score = 0.71f,
|
||||
createdAt = "4h ago",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = true,
|
||||
|
|
@ -125,7 +123,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 3,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -134,7 +132,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 4,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -143,7 +141,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 5,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -152,7 +150,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 6,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
|
@ -46,7 +45,6 @@ internal sealed interface MarketChartUM {
|
|||
|
||||
data class Content(
|
||||
val items: ImmutableList<MarketsListItemUM>,
|
||||
val triggerScrollReset: StateEvent<Unit>,
|
||||
val sortChartConfig: SortChartConfigUM,
|
||||
) : MarketChartUM
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,378 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.features.feed.model.converter.MarketsTokenItemConverter
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
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.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeedMarketsBatchFlowManager(
|
||||
private val getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val modelScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val managersByOrder = TokenMarketListConfig.Order.entries.associateWith { order ->
|
||||
createManagerForOrder(order)
|
||||
}
|
||||
|
||||
val itemsByOrder: StateFlow<Map<SortByTypeUM, ImmutableList<MarketsListItemUM>>> =
|
||||
combine(
|
||||
TokenMarketListConfig.Order.entries.mapNotNull { order ->
|
||||
managersByOrder[order]?.uiItems?.map { items -> order to items }
|
||||
},
|
||||
) { itemsList ->
|
||||
itemsList.associate { (order, items) ->
|
||||
val sortByType = order.toSortByTypeUM()
|
||||
sortByType to items
|
||||
}
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
val loadingStatesByOrder: StateFlow<Map<SortByTypeUM, Boolean>> =
|
||||
combine(
|
||||
TokenMarketListConfig.Order.entries.mapNotNull { order ->
|
||||
managersByOrder[order]?.isLoading?.map { isLoading -> order to isLoading }
|
||||
},
|
||||
) { loadingStatesList ->
|
||||
loadingStatesList.associate { (order, isLoading) ->
|
||||
val sortByType = order.toSortByTypeUM()
|
||||
sortByType to isLoading
|
||||
}
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
val errorStatesByOrder: StateFlow<Map<SortByTypeUM, Boolean>> =
|
||||
combine(
|
||||
TokenMarketListConfig.Order.entries.mapNotNull { order ->
|
||||
managersByOrder[order]?.hasError?.map { hasError -> order to hasError }
|
||||
},
|
||||
) { errorStatesList ->
|
||||
errorStatesList.associate { (order, hasError) ->
|
||||
val sortByType = order.toSortByTypeUM()
|
||||
sortByType to hasError
|
||||
}
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
init {
|
||||
managersByOrder.values.forEach { manager ->
|
||||
manager.reload(currentAppCurrency().code)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createManagerForOrder(order: TokenMarketListConfig.Order): SingleOrderManager {
|
||||
val actionsFlow = MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>()
|
||||
|
||||
val batchFlow = getTopFiveMarketTokenUseCase(
|
||||
batchingContext = TokenListBatchingContext(
|
||||
actionsFlow = actionsFlow,
|
||||
coroutineScope = modelScope,
|
||||
),
|
||||
order = order,
|
||||
)
|
||||
|
||||
return SingleOrderManager(
|
||||
order = order,
|
||||
actionsFlow = actionsFlow,
|
||||
batchFlow = batchFlow,
|
||||
currentAppCurrency = currentAppCurrency,
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
fun reloadAll() {
|
||||
managersByOrder.values.forEach { manager ->
|
||||
manager.reload(currentAppCurrency().code)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQuotes() {
|
||||
managersByOrder.values.forEach { manager ->
|
||||
manager.updateQuotes(currentAppCurrency().code)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCharts(order: TokenMarketListConfig.Order) {
|
||||
managersByOrder[order]?.loadCharts()
|
||||
}
|
||||
|
||||
fun getOnLastBatchLoadedSuccessFlow(order: TokenMarketListConfig.Order): Flow<Int>? {
|
||||
return managersByOrder[order]?.onLastBatchLoadedSuccess
|
||||
}
|
||||
|
||||
private class SingleOrderManager(
|
||||
val order: TokenMarketListConfig.Order,
|
||||
private val actionsFlow: MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>,
|
||||
private val batchFlow: TokenListBatchFlow,
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val modelScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val updateStateJob = JobHolder()
|
||||
private val resultBatches = MutableStateFlow(ResultBatches())
|
||||
private val uiBatches = resultBatches.map { it.uiBatches }
|
||||
|
||||
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>> =
|
||||
uiBatches
|
||||
.map { batches ->
|
||||
batches.asSequence()
|
||||
.map { it.data }
|
||||
.flatten()
|
||||
.toImmutableList()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = persistentListOf(),
|
||||
)
|
||||
|
||||
val isLoading = batchFlow.state
|
||||
.map { state ->
|
||||
when (state.status) {
|
||||
is PaginationStatus.InitialLoading -> true
|
||||
is PaginationStatus.NextBatchLoading -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val hasError = batchFlow.state
|
||||
.map { state ->
|
||||
when (val status = state.status) {
|
||||
is PaginationStatus.InitialLoadingError -> true
|
||||
is PaginationStatus.Paginating -> status.lastResult is BatchFetchResult.Error
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val onLastBatchLoadedSuccess = batchFlow.state
|
||||
.distinctUntilChanged { old, new -> old.status == new.status && old.data.size == new.data.size }
|
||||
.mapNotNull { batchListState ->
|
||||
when (val status = batchListState.status) {
|
||||
is PaginationStatus.Paginating -> {
|
||||
if (status.lastResult is BatchFetchResult.Success) {
|
||||
batchListState.data.lastOrNull()?.key
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
is PaginationStatus.EndOfPagination -> {
|
||||
batchListState.data.lastOrNull()?.key
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
batchFlow.state
|
||||
.map { it.data }
|
||||
.distinctUntilChanged { a, b ->
|
||||
a.size == b.size &&
|
||||
a.map { it.key } == b.map { it.key } &&
|
||||
a.map { it.data }.flatten() == b.map { it.data }.flatten()
|
||||
}
|
||||
.onEach {
|
||||
coroutineScope {
|
||||
launch {
|
||||
updateState(it)
|
||||
}.saveIn(updateStateJob)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun updateState(newList: List<Batch<Int, List<TokenMarket>>>, forceUpdate: Boolean = false) =
|
||||
withContext(dispatchers.default) {
|
||||
resultBatches.update { resultBatches ->
|
||||
val items = resultBatches.uiBatches
|
||||
val previousList = resultBatches.processedItems
|
||||
|
||||
val converter = MarketsTokenItemConverter(
|
||||
currentTrendInterval = MarketsListUM.TrendInterval.H24,
|
||||
appCurrency = currentAppCurrency(),
|
||||
)
|
||||
|
||||
if (newList.isEmpty()) {
|
||||
return@update ResultBatches(processedItems = emptyList())
|
||||
}
|
||||
|
||||
val isInitialLoading =
|
||||
forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key
|
||||
|
||||
val outItems = if (isInitialLoading) {
|
||||
newList.map { batch ->
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = converter.convertList(batch.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// As nextBatchSize = 0, we only have one batch, but keep the logic for safety
|
||||
if (previousList.size != newList.size) {
|
||||
val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet())
|
||||
val newBatches = newList.filter { keysToAdd.contains(it.key) }
|
||||
|
||||
items + newBatches.map { batch ->
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = converter.convertList(batch.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items.mapIndexed { batchIndex, batch ->
|
||||
val prevBatch = previousList[batchIndex]
|
||||
val newBatch = newList[batchIndex]
|
||||
if (prevBatch == newBatch) return@mapIndexed batch
|
||||
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = batch.data.mapIndexed { index, marketsListItemUM ->
|
||||
val prevItem = prevBatch.data.getOrNull(index)
|
||||
val newItem = newBatch.data.getOrNull(index)
|
||||
if (prevItem != null && newItem != null) {
|
||||
converter.update(prevItem, marketsListItemUM, newItem)
|
||||
} else {
|
||||
newItem?.let { converter.convert(it) } ?: marketsListItemUM
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentCoroutineContext().ensureActive()
|
||||
|
||||
ResultBatches(
|
||||
uiBatches = outItems,
|
||||
processedItems = newList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun reload(fiatPriceCurrency: String) {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
resultBatches.value = ResultBatches()
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TokenMarketListConfig(
|
||||
fiatPriceCurrency = fiatPriceCurrency,
|
||||
searchText = null,
|
||||
priceChangeInterval = TokenMarketListConfig.Interval.H24,
|
||||
order = order,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQuotes(fiatPriceCurrency: String) {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.CancelUpdates {
|
||||
it.updateRequest is TokenMarketUpdateRequest.UpdateQuotes
|
||||
},
|
||||
)
|
||||
|
||||
actionsFlow.emit(
|
||||
BatchAction.UpdateBatches(
|
||||
keys = batchFlow
|
||||
.state
|
||||
.value
|
||||
.data
|
||||
.map { it.key }
|
||||
.toSet(),
|
||||
updateRequest = TokenMarketUpdateRequest.UpdateQuotes(
|
||||
currencyId = fiatPriceCurrency,
|
||||
),
|
||||
async = true,
|
||||
operationId = "update quotes",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCharts() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
val currentData = batchFlow.state.value.data
|
||||
val alreadyLoadedChartsBatchKeys = currentData
|
||||
.filter { batch ->
|
||||
val first = batch.data.firstOrNull() ?: return@filter false
|
||||
first.tokenCharts.h24 != null
|
||||
}
|
||||
.map { it.key }
|
||||
.toSet()
|
||||
|
||||
val batchesKeysToLoad = currentData.map { it.key }.toSet().minus(alreadyLoadedChartsBatchKeys)
|
||||
|
||||
if (batchesKeysToLoad.isNotEmpty()) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.UpdateBatches(
|
||||
keys = batchesKeysToLoad,
|
||||
updateRequest = TokenMarketUpdateRequest.UpdateChart(
|
||||
interval = TokenMarketListConfig.Interval.H24,
|
||||
currency = currentAppCurrency().code,
|
||||
),
|
||||
async = true,
|
||||
operationId = batchesKeysToLoad.toString() + "h24",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class ResultBatches(
|
||||
val uiBatches: List<Batch<Int, List<MarketsListItemUM>>> = emptyList(),
|
||||
val processedItems: List<Batch<Int, List<TokenMarket>>>? = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketListConfig.Order.toSortByTypeUM(): SortByTypeUM {
|
||||
return when (this) {
|
||||
TokenMarketListConfig.Order.ByRating -> SortByTypeUM.Rating
|
||||
TokenMarketListConfig.Order.Trending -> SortByTypeUM.Trending
|
||||
TokenMarketListConfig.Order.Buyers -> SortByTypeUM.ExperiencedBuyers
|
||||
TokenMarketListConfig.Order.TopGainers -> SortByTypeUM.TopGainers
|
||||
TokenMarketListConfig.Order.TopLosers -> SortByTypeUM.TopLosers
|
||||
TokenMarketListConfig.Order.Staking -> SortByTypeUM.Staking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,19 @@ import com.tangem.common.ui.news.ArticleConfigUM
|
|||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.FormattedDate
|
||||
import com.tangem.core.ui.utils.getFormattedDate
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal class TrendingNewsStateFactory(
|
||||
private val currentStateProvider: Provider<FeedListUM>,
|
||||
|
|
@ -47,7 +54,7 @@ internal class TrendingNewsStateFactory(
|
|||
)
|
||||
},
|
||||
).toPersistentSet(),
|
||||
createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY]
|
||||
createdAt = mapFormattedDate(article.createdAt),
|
||||
isViewed = article.viewed,
|
||||
)
|
||||
},
|
||||
|
|
@ -72,7 +79,7 @@ internal class TrendingNewsStateFactory(
|
|||
)
|
||||
},
|
||||
).toPersistentSet(),
|
||||
createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY]
|
||||
createdAt = mapFormattedDate(article.createdAt),
|
||||
isViewed = article.viewed,
|
||||
)
|
||||
}.toPersistentList(),
|
||||
|
|
@ -80,4 +87,34 @@ internal class TrendingNewsStateFactory(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapFormattedDate(createdAt: String): TextReference {
|
||||
val formattedDate = getFormattedDate(
|
||||
createdAt = createdAt,
|
||||
now = DateTime.now(),
|
||||
)
|
||||
return when (formattedDate) {
|
||||
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
|
||||
is FormattedDate.HoursAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_hours_ago,
|
||||
count = formattedDate.hours,
|
||||
formatArgs = wrappedList(formattedDate.hours),
|
||||
)
|
||||
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_minutes_ago,
|
||||
count = formattedDate.minutes,
|
||||
formatArgs = wrappedList(formattedDate.minutes),
|
||||
)
|
||||
is FormattedDate.Today -> TextReference.Combined(
|
||||
refs = WrappedList(
|
||||
data = listOf(
|
||||
TextReference.Res(R.string.common_today),
|
||||
TextReference.Str(StringsSigns.COMA_SIGN),
|
||||
TextReference.Str(StringsSigns.WHITE_SPACE),
|
||||
TextReference.Str(formattedDate.time),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
package com.tangem.features.feed.ui.market.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import com.tangem.common.ui.charts.MarketChartMini
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.tokens.TokenPriceText
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.MarketsTestTags
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
import com.tangem.features.feed.ui.market.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
internal fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
MarketsListItemContent(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RectangleShape)
|
||||
.clickable(onClick = onClick)
|
||||
.testTag(MarketsTestTags.TOKENS_LIST_ITEM),
|
||||
model = model,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier = Modifier) {
|
||||
val windowSize = LocalWindowSize.current
|
||||
|
||||
Row(
|
||||
modifier = modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing15,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CoinIcon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size36),
|
||||
url = model.iconUrl,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
)
|
||||
|
||||
SpacerW12()
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TokenTitle(
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
name = model.name,
|
||||
currencySymbol = model.currencySymbol,
|
||||
)
|
||||
SpacerW8()
|
||||
TokenPriceText(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
price = model.price.text,
|
||||
priceChangeType = model.price.changeType,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
TokenSubtitle(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.alignByBaseline(),
|
||||
ratingPosition = model.ratingPosition,
|
||||
marketCap = model.marketCap,
|
||||
stakingRate = model.stakingRate,
|
||||
)
|
||||
PriceChangeInPercent(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
type = model.trendType,
|
||||
valueInPercent = model.trendPercentText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (windowSize.widthAtLeast(WindowSizeType.Small)) {
|
||||
Spacer(Modifier.width(TangemTheme.dimens.spacing10))
|
||||
|
||||
Chart(
|
||||
chartType = model.chartType,
|
||||
chartRawData = model.chartData,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier = Modifier) {
|
||||
Row(modifier = modifier) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.alignByBaseline(),
|
||||
text = name,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
SpacerW4()
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
text = currencySymbol,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenSubtitle(
|
||||
ratingPosition: String?,
|
||||
marketCap: String?,
|
||||
stakingRate: TextReference?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TokenRatingPlace(ratingPosition = ratingPosition)
|
||||
if (marketCap != null) {
|
||||
SpacerW4()
|
||||
TokenMarketCapText(
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
text = marketCap,
|
||||
)
|
||||
}
|
||||
if (stakingRate != null) {
|
||||
SpacerW4()
|
||||
StakingRate(stakingRate = stakingRate.resolveReference())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.TokenRatingPlace(ratingPosition: String?) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.heightIn(min = TangemTheme.dimens.size16)
|
||||
.background(
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing5),
|
||||
) {
|
||||
Text(
|
||||
text = ratingPosition ?: MINUS,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.StakingRate(stakingRate: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.heightIn(min = TangemTheme.dimens.size16)
|
||||
.border(
|
||||
width = TangemTheme.dimens.size1,
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing5),
|
||||
) {
|
||||
Text(
|
||||
text = stakingRate,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.TokenMarketCapText(text: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier.alignByBaseline(),
|
||||
text = text,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?) {
|
||||
val chartWidth = TangemTheme.dimens.size56
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing2)
|
||||
.size(height = TangemTheme.dimens.size24, width = chartWidth),
|
||||
) {
|
||||
if (chartRawData != null) {
|
||||
MarketChartMini(
|
||||
rawData = chartRawData,
|
||||
type = chartType,
|
||||
)
|
||||
} else {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size12)
|
||||
.align(Alignment.Center),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal")
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 260, name = "small width")
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemUM) {
|
||||
TangemThemePreview {
|
||||
var state1 by remember { mutableStateOf(state) }
|
||||
var state2 by remember { mutableStateOf(state) }
|
||||
var prices by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
100 to PriceChangeType.NEUTRAL,
|
||||
200 to PriceChangeType.NEUTRAL,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
MarketsListItem(
|
||||
modifier = Modifier,
|
||||
model = state1,
|
||||
)
|
||||
MarketsListItem(
|
||||
modifier = Modifier,
|
||||
model = state2,
|
||||
)
|
||||
Row {
|
||||
Button(
|
||||
onClick = {
|
||||
state1 = state1.copy(
|
||||
trendType = PriceChangeType.entries.random(),
|
||||
)
|
||||
state2 = state2.copy(
|
||||
trendType = PriceChangeType.entries.random(),
|
||||
)
|
||||
},
|
||||
) { Text(text = "trend") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
prices = prices.map { (price, _) ->
|
||||
if (Random.nextBoolean()) {
|
||||
price.inc() to PriceChangeType.UP
|
||||
} else {
|
||||
price.dec() to PriceChangeType.DOWN
|
||||
}
|
||||
}
|
||||
state1 = state1.copy(
|
||||
price = MarketsListItemUM.Price(
|
||||
text = "0.${prices[0].first}023 $",
|
||||
changeType = prices[0].second,
|
||||
),
|
||||
)
|
||||
state2 = state2.copy(
|
||||
price = MarketsListItemUM.Price(
|
||||
text = "0.${prices[1].first}023 $",
|
||||
changeType = prices[1].second,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { Text(text = "price") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
package com.tangem.features.feed.ui.market.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun MarketsListItemPlaceholder() {
|
||||
val density = LocalDensity.current
|
||||
val windowSize = LocalWindowSize.current
|
||||
val sp12 = with(density) { 12.sp.toDp() }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing15,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CircleShimmer(Modifier.size(TangemTheme.dimens.size36))
|
||||
|
||||
SpacerW12()
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = TangemTheme.dimens.spacing4),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size70)
|
||||
.height(sp12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = TangemTheme.dimens.spacing2),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size52)
|
||||
.height(sp12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (windowSize.widthAtLeast(WindowSizeType.Small)) {
|
||||
Spacer(Modifier.width(TangemTheme.dimens.spacing10))
|
||||
|
||||
Box {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.width(TangemTheme.dimens.size56)
|
||||
.height(TangemTheme.dimens.size12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal")
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 320, name = "small width")
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
Column(Modifier.background(TangemTheme.colors.background.tertiary)) {
|
||||
repeat(20) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
package com.tangem.features.feed.ui.market.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
|
||||
collection = listOf(
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = "",
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.NEUTRAL,
|
||||
chartData = null,
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.23348172384781234 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.DOWN,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = null,
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = null,
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = null,
|
||||
marketCap = null,
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package com.tangem.features.feed.ui.market.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
||||
@Immutable
|
||||
data class MarketsListItemUM(
|
||||
val id: CryptoCurrency.RawID,
|
||||
val name: String,
|
||||
val currencySymbol: String,
|
||||
val iconUrl: String?,
|
||||
val ratingPosition: String?,
|
||||
val marketCap: String?,
|
||||
val price: Price,
|
||||
val trendPercentText: String,
|
||||
val trendType: PriceChangeType,
|
||||
val chartData: MarketChartRawData?,
|
||||
val isUnder100kMarketCap: Boolean,
|
||||
val stakingRate: TextReference?,
|
||||
val updateTimestamp: Long?,
|
||||
) {
|
||||
val chartType: MarketChartLook.Type = when (trendType) {
|
||||
PriceChangeType.UP -> MarketChartLook.Type.Growing
|
||||
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
|
||||
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class Price(
|
||||
val text: String,
|
||||
val changeType: PriceChangeType? = null,
|
||||
)
|
||||
|
||||
@Suppress("NullableToStringCall")
|
||||
fun getComposeKey(): String {
|
||||
return id.value + TOKEN_LAZY_LIST_ID_SEPARATOR + marketCap.toString() + updateTimestamp
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TOKEN_LAZY_LIST_ID_SEPARATOR = "@"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.feed.ui.market.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue