Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-15 10:48:01 +01:00
parent b0ac045d0d
commit 626d0914a0
31 changed files with 1201 additions and 95 deletions

View file

@ -10,7 +10,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ComponentModule {
internal interface FeedComponentModule {
@Binds
@Singleton

View file

@ -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() }
}
}

View file

@ -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
}
}

View file

@ -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 ->

View file

@ -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() {

View file

@ -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,

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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),
),
),
)
}
}
}

View file

@ -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") }
}
}
}
}

View file

@ -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()
}
}
}
}

View file

@ -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,
),
),
)

View file

@ -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 = "@"
}
}

View file

@ -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