Updated on 2026-08-14
This commit is contained in:
parent
ca9e191cb8
commit
ba08d91950
16 changed files with 1760 additions and 12 deletions
|
|
@ -0,0 +1,388 @@
|
|||
package com.tangem.features.feed.ui.feed
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
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.FeedListCallbacks
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
import com.tangem.features.feed.ui.feed.state.MarketChartConfig
|
||||
import com.tangem.features.feed.ui.feed.state.MarketChartUM
|
||||
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
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
|
||||
val density = LocalDensity.current
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.drawBehind { drawRect(background) },
|
||||
) {
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp,
|
||||
)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
if (coordinates.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(coordinates.size.height.toDp())
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(bottom = 4.dp),
|
||||
state = state.searchBar,
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
text = stringResourceSafe(R.string.feed_market_and_news),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
text = state.currentDate,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
|
||||
MarketBlock(
|
||||
marketChartConfig = state.marketChartConfig,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
)
|
||||
|
||||
NewsBlock(
|
||||
news = state.news,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
trendingArticle = state.trendingArticle,
|
||||
)
|
||||
|
||||
MarketPulseBlock(
|
||||
marketChartConfig = state.marketChartConfig,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
items(
|
||||
items = marketChartConfig.getFilterPreset(),
|
||||
key = SortByTypeUM::name,
|
||||
) { sortByTypeUM ->
|
||||
FilterChip(
|
||||
sortByTypeUM = sortByTypeUM,
|
||||
isSelected = sortByTypeUM == marketChartConfig.currentSortByType,
|
||||
onClick = { feedListCallbacks.onSortTypeClick(sortByTypeUM) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) },
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
AnimatedContent(
|
||||
targetState = marketChartConfig.currentSortByType,
|
||||
label = "MarketPulseChartAnimation",
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
) { currentSortType ->
|
||||
marketChartConfig.marketCharts[currentSortType]?.let { chart ->
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
marketChart = chart,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CanBeNonNullable")
|
||||
@Composable
|
||||
private fun NewsBlock(
|
||||
feedListCallbacks: FeedListCallbacks,
|
||||
news: ImmutableList<ArticleConfigUM>,
|
||||
trendingArticle: ArticleConfigUM?,
|
||||
) {
|
||||
if (news.isNotEmpty()) {
|
||||
Header(
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
SpacerW(4.dp)
|
||||
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_stars_20),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW(2.dp)
|
||||
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
SpanStyle().copy(
|
||||
brush = Brush.linearGradient(
|
||||
GRADIENT_START to LinearGradientFirstPart,
|
||||
GRADIENT_END to LinearGradientSecondPart,
|
||||
),
|
||||
),
|
||||
) {
|
||||
append(stringResourceSafe(R.string.feed_tangem_ai))
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = feedListCallbacks.onOpenAllNews,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
trendingArticle?.let { article ->
|
||||
ArticleCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
items(
|
||||
items = news,
|
||||
key = ArticleConfigUM::id,
|
||||
) { article ->
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
modifier = Modifier.size(164.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
title()
|
||||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.common_see_all),
|
||||
onClick = onSeeAllClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Charts(
|
||||
marketChart: MarketChartUM,
|
||||
onItemClick: (MarketsListItemUM) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BlockCard(modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
when (marketChart) {
|
||||
MarketChartUM.Loading -> {
|
||||
repeat(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
is MarketChartUM.LoadingError -> {
|
||||
// TODO will be created in [REDACTED_TASK_KEY]
|
||||
}
|
||||
is MarketChartUM.Content -> {
|
||||
marketChart.items.fastForEach { chart ->
|
||||
MarketsListItem(
|
||||
model = chart,
|
||||
onClick = { onItemClick(chart) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FilterChip(sortByTypeUM: SortByTypeUM, isSelected: Boolean, onClick: () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(shape = RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
color = if (isSelected) {
|
||||
TangemTheme.colors.button.primary
|
||||
} else {
|
||||
TangemTheme.colors.button.secondary
|
||||
},
|
||||
)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = sortByTypeUM.text.resolveReference(),
|
||||
style = TangemTheme.typography.button,
|
||||
color = if (isSelected) {
|
||||
TangemTheme.colors.text.primary2
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_CHART_SIZE_IN_MARKET = 5
|
||||
private const val GRADIENT_START = 0f
|
||||
private const val GRADIENT_END = 0.5f
|
||||
private val LinearGradientFirstPart = Color(0xFF635EEC)
|
||||
private val LinearGradientSecondPart = Color(0xFFE05AED)
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun FeedListPreview() {
|
||||
TangemThemePreview {
|
||||
FeedList(
|
||||
state = createFeedPreviewState(),
|
||||
onHeaderSizeChange = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
package com.tangem.features.feed.ui.feed.preview
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.common.ui.news.ArticleTagUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
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.*
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal object FeedListPreviewDataProvider {
|
||||
|
||||
fun createFeedPreviewState(): FeedListUM {
|
||||
val articles = createSampleArticles()
|
||||
val marketItems = createSampleMarketItems()
|
||||
return FeedListUM(
|
||||
currentDate = "20 November",
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = TextReference.Str("Search tokens & news"),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = {},
|
||||
),
|
||||
feedListCallbacks = FeedListCallbacks(
|
||||
onSearchClick = {},
|
||||
onMarketOpenClick = {},
|
||||
onArticleClick = {},
|
||||
onOpenAllNews = {},
|
||||
onMarketItemClick = {},
|
||||
onSortTypeClick = {},
|
||||
),
|
||||
news = articles.filter { it.isTrending.not() }.toImmutableList(),
|
||||
trendingArticle = articles.first { it.isTrending },
|
||||
marketChartConfig = MarketChartConfig(
|
||||
marketCharts = createMarketCharts(marketItems, includeErrorState = false),
|
||||
currentSortByType = SortByTypeUM.TopGainers,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMarketCharts(
|
||||
items: ImmutableList<MarketsListItemUM>,
|
||||
includeErrorState: Boolean,
|
||||
): ImmutableMap<SortByTypeUM, MarketChartUM> {
|
||||
val baseCharts = mapOf(
|
||||
SortByTypeUM.TopGainers to createChartContent(
|
||||
sortByType = SortByTypeUM.TopGainers,
|
||||
items = items,
|
||||
isSelected = true,
|
||||
),
|
||||
SortByTypeUM.Trending to createChartContent(
|
||||
sortByType = SortByTypeUM.Trending,
|
||||
items = items,
|
||||
isSelected = false,
|
||||
),
|
||||
SortByTypeUM.ExperiencedBuyers to createChartContent(
|
||||
sortByType = SortByTypeUM.ExperiencedBuyers,
|
||||
items = items,
|
||||
isSelected = false,
|
||||
),
|
||||
SortByTypeUM.Staking to MarketChartUM.Loading,
|
||||
SortByTypeUM.TopLosers to MarketChartUM.Loading,
|
||||
)
|
||||
|
||||
val ratingChart = if (includeErrorState) {
|
||||
MarketChartUM.LoadingError(onRetryClicked = {})
|
||||
} else {
|
||||
createChartContent(
|
||||
sortByType = SortByTypeUM.Rating,
|
||||
items = items,
|
||||
isSelected = false,
|
||||
)
|
||||
}
|
||||
|
||||
return persistentMapOf(
|
||||
SortByTypeUM.Rating to ratingChart,
|
||||
*baseCharts.entries.map { it.toPair() }.toTypedArray(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createChartContent(
|
||||
sortByType: SortByTypeUM,
|
||||
items: ImmutableList<MarketsListItemUM>,
|
||||
isSelected: Boolean,
|
||||
): MarketChartUM.Content {
|
||||
return MarketChartUM.Content(
|
||||
items = items,
|
||||
triggerScrollReset = consumedEvent(),
|
||||
sortChartConfig = SortChartConfigUM(
|
||||
sortByType = sortByType,
|
||||
isSelected = isSelected,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSampleArticles(): ImmutableList<ArticleConfigUM> = persistentListOf(
|
||||
ArticleConfigUM(
|
||||
id = 1,
|
||||
title = "Bitcoin ETF reaches new highs, institutions pile in",
|
||||
score = 0.82f,
|
||||
createdAt = "2h ago",
|
||||
isTrending = true,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
),
|
||||
ArticleConfigUM(
|
||||
id = 2,
|
||||
title = "Layer 2 networks battle for dominance amid fee wars",
|
||||
score = 0.71f,
|
||||
createdAt = "4h ago",
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = true,
|
||||
),
|
||||
ArticleConfigUM(
|
||||
id = 3,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
),
|
||||
ArticleConfigUM(
|
||||
id = 4,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
),
|
||||
ArticleConfigUM(
|
||||
id = 5,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
),
|
||||
ArticleConfigUM(
|
||||
id = 6,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
),
|
||||
)
|
||||
|
||||
private fun createArticleTags(): ImmutableSet<ArticleTagUM> {
|
||||
return persistentSetOf(
|
||||
ArticleTagUM.Token(
|
||||
title = TextReference.Str("BTC"),
|
||||
iconState = CurrencyIconState.CoinIcon(
|
||||
url = "",
|
||||
fallbackResId = 0,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
),
|
||||
ArticleTagUM.Category(TextReference.Str("Regulation")),
|
||||
ArticleTagUM.Category(TextReference.Str("BTC")),
|
||||
ArticleTagUM.Category(TextReference.Str("Supply")),
|
||||
ArticleTagUM.Category(TextReference.Str("Demand")),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSampleMarketItems(): ImmutableList<MarketsListItemUM> = persistentListOf(
|
||||
createMarketItem(
|
||||
id = "btc",
|
||||
name = "Bitcoin",
|
||||
symbol = "BTC",
|
||||
trendType = PriceChangeType.UP,
|
||||
rating = "1",
|
||||
marketCap = "$1.2T",
|
||||
percent = "12.3%",
|
||||
),
|
||||
createMarketItem(
|
||||
id = "eth",
|
||||
name = "Ethereum",
|
||||
symbol = "ETH",
|
||||
trendType = PriceChangeType.DOWN,
|
||||
rating = "2",
|
||||
marketCap = "$480B",
|
||||
percent = "-3.1%",
|
||||
),
|
||||
createMarketItem(
|
||||
id = "sol",
|
||||
name = "Solana",
|
||||
symbol = "SOL",
|
||||
trendType = PriceChangeType.NEUTRAL,
|
||||
rating = "7",
|
||||
marketCap = "$110B",
|
||||
percent = "0.4%",
|
||||
),
|
||||
createMarketItem(
|
||||
id = "bnb",
|
||||
name = "BNB",
|
||||
symbol = "BNB",
|
||||
trendType = PriceChangeType.NEUTRAL,
|
||||
rating = "7",
|
||||
marketCap = "$110B",
|
||||
percent = "0.4%",
|
||||
),
|
||||
createMarketItem(
|
||||
id = "doge",
|
||||
name = "Dodge",
|
||||
symbol = "DOG",
|
||||
trendType = PriceChangeType.NEUTRAL,
|
||||
rating = "7",
|
||||
marketCap = "$110B",
|
||||
percent = "0.4%",
|
||||
),
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun createMarketItem(
|
||||
id: String,
|
||||
name: String,
|
||||
symbol: String,
|
||||
trendType: PriceChangeType,
|
||||
rating: String,
|
||||
marketCap: String,
|
||||
percent: String,
|
||||
): MarketsListItemUM {
|
||||
return MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID(id),
|
||||
name = name,
|
||||
currencySymbol = symbol,
|
||||
iconUrl = null,
|
||||
ratingPosition = rating,
|
||||
marketCap = marketCap,
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = percent,
|
||||
trendType = trendType,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
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
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal data class FeedListUM(
|
||||
val currentDate: String,
|
||||
val searchBar: SearchBarUM,
|
||||
val feedListCallbacks: FeedListCallbacks,
|
||||
val news: ImmutableList<ArticleConfigUM>,
|
||||
val trendingArticle: ArticleConfigUM?,
|
||||
val marketChartConfig: MarketChartConfig,
|
||||
)
|
||||
|
||||
internal data class FeedListCallbacks(
|
||||
val onSearchClick: () -> Unit,
|
||||
val onMarketOpenClick: (sortBy: SortByTypeUM) -> Unit,
|
||||
val onArticleClick: (id: Int) -> Unit,
|
||||
val onOpenAllNews: () -> Unit,
|
||||
val onMarketItemClick: (MarketsListItemUM) -> Unit,
|
||||
val onSortTypeClick: (SortByTypeUM) -> Unit,
|
||||
)
|
||||
|
||||
internal data class MarketChartConfig(
|
||||
val marketCharts: ImmutableMap<SortByTypeUM, MarketChartUM>,
|
||||
val currentSortByType: SortByTypeUM = SortByTypeUM.TopGainers,
|
||||
) {
|
||||
fun getFilterPreset() = SortByTypeUM.entries.filter { it != SortByTypeUM.Rating }.toPersistentList()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal sealed interface MarketChartUM {
|
||||
|
||||
data class Content(
|
||||
val items: ImmutableList<MarketsListItemUM>,
|
||||
val triggerScrollReset: StateEvent<Unit>,
|
||||
val sortChartConfig: SortChartConfigUM,
|
||||
) : MarketChartUM
|
||||
|
||||
data object Loading : MarketChartUM
|
||||
|
||||
data class LoadingError(val onRetryClicked: () -> Unit) : MarketChartUM
|
||||
}
|
||||
|
||||
data class SortChartConfigUM(
|
||||
val sortByType: SortByTypeUM,
|
||||
val isSelected: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
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 = "@"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.features.feed.ui.market.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal data class MarketsListUM(
|
||||
val list: ListUM,
|
||||
val searchBar: SearchBarUM,
|
||||
val selectedSortBy: SortByTypeUM,
|
||||
val sortByBottomSheet: TangemBottomSheetConfig,
|
||||
val selectedInterval: TrendInterval,
|
||||
val onIntervalClick: (TrendInterval) -> Unit,
|
||||
val onSortByButtonClick: () -> Unit,
|
||||
val stakingNotificationMaxApy: BigDecimal?,
|
||||
val onStakingNotificationClick: () -> Unit,
|
||||
val onStakingNotificationCloseClick: () -> Unit,
|
||||
) {
|
||||
val isInSearchMode
|
||||
get() = searchBar.isActive
|
||||
|
||||
enum class TrendInterval(val text: TextReference) {
|
||||
H24(resourceReference(R.string.markets_selector_interval_24h_title)),
|
||||
D7(resourceReference(R.string.markets_selector_interval_7d_title)),
|
||||
M1(resourceReference(R.string.markets_selector_interval_1m_title)),
|
||||
}
|
||||
}
|
||||
|
||||
enum class SortByTypeUM(val text: TextReference) {
|
||||
Rating(resourceReference(R.string.markets_sort_by_rating_title)),
|
||||
Trending(resourceReference(R.string.markets_sort_by_trending_title)),
|
||||
ExperiencedBuyers(resourceReference(R.string.markets_sort_by_experienced_buyers_title)),
|
||||
TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)),
|
||||
TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)),
|
||||
Staking(resourceReference(R.string.common_staking)),
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class ListUM {
|
||||
|
||||
data class Content(
|
||||
val items: ImmutableList<MarketsListItemUM>,
|
||||
val shouldShowUnder100kTokensNotification: Boolean,
|
||||
val shouldShowUnder100kTokensNotificationWasHidden: Boolean,
|
||||
val loadMore: () -> Unit,
|
||||
val visibleIdsChanged: (List<CryptoCurrency.RawID>) -> Unit,
|
||||
val onShowTokensUnder100kClicked: () -> Unit,
|
||||
val triggerScrollReset: StateEvent<Unit>,
|
||||
val onItemClick: (MarketsListItemUM) -> Unit,
|
||||
) : ListUM()
|
||||
|
||||
data object Loading : ListUM()
|
||||
|
||||
data class LoadingError(
|
||||
val onRetryClicked: () -> Unit,
|
||||
) : ListUM()
|
||||
|
||||
data object SearchNothingFound : ListUM()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue