Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-06 11:40:31 +02:00
parent 2e857d5e3e
commit 45fa120411
49 changed files with 669 additions and 92 deletions

View file

@ -39,6 +39,7 @@ import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
@ -47,6 +48,7 @@ import com.tangem.core.ui.test.MarketsTestTags
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.windowsize.WindowSizeType
import com.tangem.utils.StringsSigns.MINUS
import java.math.BigDecimal
import kotlin.random.Random
@Composable
@ -88,6 +90,8 @@ fun MarketListItemContentV2(model: MarketsListItemUM, modifier: Modifier = Modif
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
price = model.price.text,
priceChangeType = model.price.changeType,
priceAnnotated = model.price.annotated,
priceValue = model.price.fiatPrice,
)
TokenSubtitle(
@ -127,7 +131,7 @@ private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier
.alignByBaseline(),
text = name,
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.bodySemibold16,
style = TangemTheme.typography2.bodyMedium16,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@ -323,12 +327,16 @@ private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::cl
price = MarketsListItemUM.Price(
text = "0.${prices[0].first}023 $",
changeType = prices[0].second,
fiatPrice = BigDecimal(123123),
annotated = stringReference("0.${prices[0].first}023 $"),
),
)
state2 = state2.copy(
price = MarketsListItemUM.Price(
text = "0.${prices[1].first}023 $",
changeType = prices[1].second,
fiatPrice = BigDecimal(123123),
annotated = stringReference("0.${prices[0].first}023 $"),
),
)
},

View file

@ -0,0 +1,22 @@
package com.tangem.common.ui.markets
import androidx.compose.ui.text.SpanStyle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.formatStyled
import com.tangem.core.ui.format.bigdecimal.price
import com.tangem.core.ui.res.TangemTheme
import java.math.BigDecimal
/**
* Fiat amount for markets list rows: integer part in primary style, fractional part (from locale separator) in secondary.
*/
fun BigDecimal.toMarketsListItemPriceAnnotated(appCurrencyCode: String, appCurrencySymbol: String): TextReference {
return formatStyled {
fiat(
fiatCurrencyCode = appCurrencyCode,
fiatCurrencySymbol = appCurrencySymbol,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
).price()
}
}

View file

@ -29,12 +29,14 @@ 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.extensions.stringReference
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.utils.StringsSigns.MINUS
import java.math.BigDecimal
import kotlin.random.Random
@Composable
@ -85,6 +87,8 @@ private fun MarketsListItemContentV1(model: MarketsListItemUM, modifier: Modifie
modifier = Modifier.alignByBaseline(),
price = model.price.text,
priceChangeType = model.price.changeType,
priceAnnotated = model.price.annotated,
priceValue = model.price.fiatPrice,
)
}
@ -306,12 +310,16 @@ private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::cl
price = MarketsListItemUM.Price(
text = "0.${prices[0].first}023 $",
changeType = prices[0].second,
fiatPrice = BigDecimal(123123),
annotated = stringReference("0.${prices[0].first}023 $"),
),
)
state2 = state2.copy(
price = MarketsListItemUM.Price(
text = "0.${prices[1].first}023 $",
changeType = prices[1].second,
fiatPrice = BigDecimal(123123),
annotated = stringReference("0.${prices[0].first}023 $"),
),
)
},

View file

@ -6,6 +6,7 @@ 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
import java.math.BigDecimal
@Immutable
data class MarketsListItemUM(
@ -40,7 +41,9 @@ data class MarketsListItemUM(
@Immutable
data class Price(
val text: String,
val annotated: TextReference,
val changeType: PriceChangeType? = null,
val fiatPrice: BigDecimal,
)
@Suppress("NullableToStringCall")

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.currency.CryptoCurrency
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
@Suppress("MagicNumber")
class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
@ -18,7 +19,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
iconUrl = "",
ratingPosition = "1",
marketCap = "$6.233 B",
price = MarketsListItemUM.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(
text = "31 285.72$",
fiatPrice = BigDecimal(123123),
annotated = stringReference("31 285.72$"),
),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chartData = MarketChartRawData(
@ -35,7 +40,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
iconUrl = null,
ratingPosition = "2",
marketCap = "$6.233 B",
price = MarketsListItemUM.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(
text = "31 285.72$",
fiatPrice = BigDecimal(123123),
annotated = stringReference("31 285.72$"),
),
trendPercentText = "12.43%",
trendType = PriceChangeType.NEUTRAL,
chartData = null,
@ -50,7 +59,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
iconUrl = null,
ratingPosition = "10",
marketCap = "$6.23348172384781234 B",
price = MarketsListItemUM.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(
text = "31 285.72$",
fiatPrice = BigDecimal(123123),
annotated = stringReference("31 285.72$"),
),
trendPercentText = "12.43%",
trendType = PriceChangeType.DOWN,
chartData = MarketChartRawData(
@ -67,7 +80,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
iconUrl = null,
ratingPosition = "10",
marketCap = null,
price = MarketsListItemUM.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(
text = "31 285.72$",
fiatPrice = BigDecimal(123123),
annotated = stringReference("31 285.72$"),
),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chartData = MarketChartRawData(
@ -84,7 +101,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
iconUrl = null,
ratingPosition = null,
marketCap = "$6.233 B",
price = MarketsListItemUM.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(
text = "31 285.72$",
fiatPrice = BigDecimal(123123),
annotated = stringReference("31 285.72$"),
),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chartData = MarketChartRawData(
@ -101,7 +122,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide
iconUrl = null,
ratingPosition = null,
marketCap = null,
price = MarketsListItemUM.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(
text = "31 285.72$",
fiatPrice = BigDecimal(123123),
annotated = stringReference("31 285.72$"),
),
trendPercentText = "12.43%",
trendType = PriceChangeType.UP,
chartData = MarketChartRawData(

View file

@ -6,25 +6,33 @@ import androidx.compose.animation.core.tween
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import java.math.BigDecimal
/**
* Text view for token price.
*
* @param price Price of the token.
* @param price Plain price string (used by V1 and as fallback for V2 when [priceAnnotated] is null).
* @param priceAnnotated Optional styled price (locale-aware decimal separator + secondary fractional color).
* @param priceChangeType Type of the price change.
*/
@Composable
fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
fun TokenPriceText(
priceValue: BigDecimal,
price: String,
priceAnnotated: TextReference,
modifier: Modifier = Modifier,
priceChangeType: PriceChangeType? = null,
) {
if (LocalRedesignEnabled.current) {
TokenPriceTextV2(
price = price,
priceValue = priceValue,
priceAnnotated = priceAnnotated,
modifier = modifier,
priceChangeType = priceChangeType,
)
@ -76,16 +84,20 @@ private fun TokenPriceTextV1(price: String, modifier: Modifier = Modifier, price
}
@Composable
private fun TokenPriceTextV2(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
private fun TokenPriceTextV2(
priceValue: BigDecimal,
priceAnnotated: TextReference,
modifier: Modifier = Modifier,
priceChangeType: PriceChangeType? = null,
) {
val growColor = TangemTheme.colors2.text.status.accent
val fallColor = TangemTheme.colors2.text.status.warning
val generalColor = TangemTheme.colors2.text.neutral.primary
val decimalColor = TangemTheme.colors2.text.neutral.secondary
val color = remember(generalColor) { Animatable(generalColor) }
var isAnimationSkipped by remember { mutableStateOf(false) }
LaunchedEffect(price) {
LaunchedEffect(priceValue) {
if (!isAnimationSkipped) {
isAnimationSkipped = true
return@LaunchedEffect
@ -103,27 +115,9 @@ private fun TokenPriceTextV2(price: String, modifier: Modifier = Modifier, price
}
}
val annotatedText = remember(price) {
buildAnnotatedString {
val dotIndex = price.indexOf(".")
if (dotIndex == -1) {
append(price)
} else {
append(price.take(dotIndex))
withStyle(
style = SpanStyle(color = decimalColor),
) {
append(price.substring(dotIndex))
}
}
}
}
Text(
modifier = modifier,
text = annotatedText,
text = priceAnnotated.resolveAnnotatedReference(),
color = color.value,
maxLines = 1,
style = TangemTheme.typography2.bodySemibold16,

View file

@ -116,6 +116,8 @@ data class TokenMarketInfoResponse(
val maxSupply: BigDecimal?,
@Json(name = "fully_diluted_valuation")
val fullyDilutedValuation: BigDecimal?,
@Json(name = "fully_diluted_valuation_change_24h")
val fullyDilutedValuationChange24H: BigDecimal?,
)
@JsonClass(generateAdapter = true)

View file

@ -172,6 +172,40 @@ fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value ->
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
}
/**
* Formats fiat price with precision calculated based on the value.
* Styled version fractional part uses [spanStyleReference].
* @see getFiatPriceAmountWithScale
*/
fun BigDecimalFiatFormatStyled.price() = price(spanStyleReference)
private fun BigDecimalFiatFormatStyled.price(spanStyleReference: SpanStyleReference) = BigDecimalFormatStyled { value ->
val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode)
val (priceAmount, finalScale) = getFiatPriceAmountWithScale(value = value)
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
currency = formatterCurrency
maximumFractionDigits = finalScale
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
roundingMode = RoundingMode.HALF_UP
}
val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator
val formattedAmount = formatter.format(priceAmount)
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length
val wholePart = formattedAmount.take(separatorIndex)
val fractionalPart = formattedAmount.drop(separatorIndex)
combinedReference(
stringReference(wholePart),
styledStringReference(fractionalPart, spanStyleReference),
)
}
/**
* Formats fiat amount with an exact number of fractional digits.
*/

View file

@ -198,6 +198,18 @@ class TangemTypography2 internal constructor(
),
)
val bodyMedium16: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = -0.32f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val bodyRegular15: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 15.sp,

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M4,8.667C4.368,8.667 4.667,8.965 4.667,9.333V12C4.667,12.368 4.368,12.667 4,12.667C3.632,12.667 3.333,12.368 3.333,12V9.333C3.333,8.965 3.632,8.667 4,8.667ZM8,6C8.368,6 8.667,6.298 8.667,6.667V12C8.667,12.368 8.368,12.667 8,12.667C7.632,12.667 7.333,12.368 7.333,12V6.667C7.333,6.298 7.632,6 8,6ZM12,3.333C12.368,3.333 12.667,3.632 12.667,4V12C12.667,12.368 12.368,12.667 12,12.667C11.632,12.667 11.333,12.368 11.333,12V4C11.333,3.632 11.632,3.333 12,3.333Z"
android:fillColor="#656565"/>
</vector>

View file

@ -111,6 +111,7 @@ internal class TokenMarketInfoConverter(
volume24h = volume24h,
maxSupply = maxSupply,
fullyDilutedValuation = fullyDilutedValuation,
fullyDilutedValuationChange24 = fullyDilutedValuationChange24H,
)
}

View file

@ -5,7 +5,9 @@ import com.tangem.data.search.model.TextHintDTO
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.search.model.RecentSearchToken
import com.tangem.domain.search.model.SearchTextHint
import com.tangem.domain.search.model.TokenPriceChangeDirection
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class TextHintDTOToSearchTextHintConverter : Converter<TextHintDTO, SearchTextHint> {
override fun convert(value: TextHintDTO): SearchTextHint {
@ -24,6 +26,15 @@ internal class RecentTokenDTOToRecentSearchTokenConverter : Converter<RecentToke
symbol = value.symbol,
imageUrl = value.imageUrl,
timestamp = value.timestamp,
appCurrencyCode = value.appCurrencyCode,
appCurrencySymbol = value.appCurrencySymbol,
price = value.pricePlain.toBigDecimalOrNull() ?: BigDecimal.ZERO,
priceChangePercent = value.priceChangePercent,
priceChangeDirection = value.priceChangeDirection.toTokenPriceChangeDirection(),
marketCap = value.marketCap,
ratingPosition = value.ratingPosition,
isUnder100kMarketCap = value.isUnder100kMarketCap,
stakingRateText = value.stakingRateText,
)
}
}
@ -36,6 +47,20 @@ internal class RecentSearchTokenToRecentTokenDTOConverter : Converter<RecentSear
symbol = value.symbol,
imageUrl = value.imageUrl,
timestamp = value.timestamp,
pricePlain = value.price.toPlainString(),
appCurrencyCode = value.appCurrencyCode,
appCurrencySymbol = value.appCurrencySymbol,
priceChangePercent = value.priceChangePercent,
priceChangeDirection = value.priceChangeDirection.name,
marketCap = value.marketCap,
ratingPosition = value.ratingPosition,
isUnder100kMarketCap = value.isUnder100kMarketCap,
stakingRateText = value.stakingRateText,
)
}
}
private fun String.toTokenPriceChangeDirection(): TokenPriceChangeDirection {
return runCatching { TokenPriceChangeDirection.valueOf(this) }
.getOrDefault(TokenPriceChangeDirection.NEUTRAL)
}

View file

@ -14,6 +14,9 @@ internal data class TextHintDTO(
val timestamp: Long,
)
/**
* @param appCurrencyCode currency code (e.g. "USD") the [pricePlain] was quoted in
*/
@JsonClass(generateAdapter = true)
internal data class RecentTokenDTO(
val id: String,
@ -21,4 +24,13 @@ internal data class RecentTokenDTO(
val symbol: String,
val imageUrl: String?,
val timestamp: Long,
val pricePlain: String,
val appCurrencyCode: String,
val appCurrencySymbol: String,
val priceChangePercent: String,
val priceChangeDirection: String,
val marketCap: String?,
val ratingPosition: String?,
val isUnder100kMarketCap: Boolean,
val stakingRateText: String?,
)

View file

@ -55,6 +55,7 @@ data class TokenMarketInfo(
val volume24h: BigDecimal?,
val maxSupply: BigDecimal?,
val fullyDilutedValuation: BigDecimal?,
val fullyDilutedValuationChange24: BigDecimal?,
)
data class SecurityData(

View file

@ -1,9 +1,11 @@
package com.tangem.domain.search.model
import com.tangem.domain.models.currency.CryptoCurrency
import java.math.BigDecimal
/**
* @property timestamp epoch milliseconds
* @property price fiat quote in the app currency that was active when the token was saved
*/
data class RecentSearchToken(
val id: CryptoCurrency.RawID,
@ -11,4 +13,13 @@ data class RecentSearchToken(
val symbol: String,
val imageUrl: String?,
val timestamp: Long,
val appCurrencyCode: String,
val appCurrencySymbol: String,
val price: BigDecimal,
val priceChangePercent: String,
val priceChangeDirection: TokenPriceChangeDirection,
val marketCap: String?,
val ratingPosition: String?,
val isUnder100kMarketCap: Boolean,
val stakingRateText: String?,
)

View file

@ -0,0 +1,7 @@
package com.tangem.domain.search.model
enum class TokenPriceChangeDirection {
UP,
DOWN,
NEUTRAL,
}

View file

@ -35,10 +35,11 @@ internal class EarnTokenWithCurrencyToListItemUMConverter(
formatArgs = wrappedList(convertPercent(value.earnToken.apy)),
)
},
earnType = when (value.earnToken.type) {
earnTypeTitle = when (value.earnToken.type) {
EarnType.STAKING -> TextReference.Res(R.string.common_staking)
EarnType.YIELD -> TextReference.Res(R.string.common_yield_mode)
},
earnType = value.earnToken.type,
onItemClick = { onItemClick(value) },
)
}

View file

@ -5,6 +5,7 @@ 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.common.ui.markets.toMarketsListItemPriceAnnotated
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
@ -110,7 +111,12 @@ internal class MarketsTokenItemConverter(
return MarketsListItemUM.Price(
text = priceText,
annotated = tokenQuotesShort.currentPrice.toMarketsListItemPriceAnnotated(
appCurrencyCode = appCurrency.code,
appCurrencySymbol = appCurrency.symbol,
),
changeType = changeType,
fiatPrice = tokenQuotesShort.currentPrice,
)
}

View file

@ -230,6 +230,9 @@ internal class MarketsTokenDetailsModel @Inject constructor(
fiatCurrencySymbol = currentAppCurrency.value.symbol,
).price()
},
priceAnnotated = params.token.tokenQuotes.currentPrice.toMarketsTokenDetailsPriceAnnotated(
currentAppCurrency.value,
),
dateTimeText = resourceReference(R.string.common_today),
priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() },
priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(),
@ -505,6 +508,9 @@ internal class MarketsTokenDetailsModel @Inject constructor(
fiatCurrencyCode = currentAppCurrency.value.code,
).price()
},
priceAnnotated = newInfo.quotes.currentPrice.toMarketsTokenDetailsPriceAnnotated(
currentAppCurrency.value,
),
priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval(
interval = marketsTokenDetailsUM.selectedInterval,
),
@ -604,7 +610,8 @@ internal class MarketsTokenDetailsModel @Inject constructor(
)
} ?: getDefaultDateTimeString(currentState.selectedInterval)
val priceText = (price ?: currentQuotes.value.currentPrice).format {
val amountForPrice = price ?: currentQuotes.value.currentPrice
val priceText = amountForPrice.format {
fiat(
fiatCurrencySymbol = currentAppCurrency.value.symbol,
fiatCurrencyCode = currentAppCurrency.value.code,
@ -625,6 +632,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
isMarkerSet = markerTimestamp != null,
dateTimeText = dateTimeText,
priceText = priceText,
priceAnnotated = amountForPrice.toMarketsTokenDetailsPriceAnnotated(currentAppCurrency.value),
priceChangePercentText = percentText,
priceChangeType = percent.percentChangeType(),
)

View file

@ -1,10 +1,7 @@
package com.tangem.features.feed.model.market.details.converter
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.compact
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
@ -188,6 +185,7 @@ internal class MetricsConverter(
formatArgs = wrappedList(formatted),
)
},
fullyDilutedValuationChange24 = fullyDilutedValuationChange24?.convertChange(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -280,6 +278,17 @@ internal class MetricsConverter(
}
}
private fun BigDecimal?.convertChange(): TextReference? {
if (this == null) return null
val amount = this.abs().formatAmount()
val value = when {
this > BigDecimal.ZERO -> StringsSigns.PLUS + amount
this < BigDecimal.ZERO -> StringsSigns.MINUS + amount
else -> amount
}
return value?.let(::stringReference)
}
@Suppress("MagicNumber")
private fun getLiquidity(volume24h: BigDecimal?, marketCap: BigDecimal?): Float? {
if (volume24h == null || marketCap == null || marketCap == BigDecimal.ZERO) return null

View file

@ -1,10 +1,12 @@
package com.tangem.features.feed.model.market.details.formatter
import androidx.compose.ui.text.SpanStyle
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.getFiatPriceAmountWithScale
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.format.bigdecimal.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenQuotes
import java.math.BigDecimal
@ -74,3 +76,13 @@ internal fun PriceChangeType.toChartType(): MarketChartLook.Type {
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral
}
}
internal fun BigDecimal.toMarketsTokenDetailsPriceAnnotated(appCurrency: AppCurrency): TextReference {
return formatStyled {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.tertiary) },
).price()
}
}

View file

@ -66,6 +66,7 @@ internal class QuotesStateUpdater(
fiatCurrencyCode = currentAppCurrency().code,
).price()
},
priceAnnotated = newQuotes.currentPrice.toMarketsTokenDetailsPriceAnnotated(currentAppCurrency()),
priceChangePercentText = newQuotes.getFormattedPercentByInterval(
interval = stateToUpdate.selectedInterval,
),

View file

@ -6,6 +6,7 @@ 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.common.ui.markets.toMarketsListItemPriceAnnotated
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.format.bigdecimal.*
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
@ -68,7 +69,12 @@ internal class RelatedTokenConverter(private val appCurrency: AppCurrency) :
return MarketsListItemUM.Price(
text = priceText,
annotated = tokenInfo.quotes.currentPrice.toMarketsListItemPriceAnnotated(
appCurrencyCode = appCurrency.code,
appCurrencySymbol = appCurrency.symbol,
),
changeType = null,
fiatPrice = tokenInfo.quotes.currentPrice,
)
}

View file

@ -5,19 +5,27 @@ import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.common.ui.charts.state.MarketChartData
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
import com.tangem.common.ui.charts.state.sorted
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.GetTokenPriceChartUseCase
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.models.account.AccountName
import com.tangem.domain.search.model.RecentSearchToken
import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase
import com.tangem.domain.search.usecase.GetSearchResultsUseCase
import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase
import com.tangem.domain.search.usecase.SaveSearchQueryUseCase
import com.tangem.features.feed.components.search.DefaultSearchComponent
import com.tangem.features.feed.model.market.list.state.MarketsListUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.model.market.list.statemanager.MarketsListBatchFlowManager
import com.tangem.features.feed.model.search.converter.MarketsListItemUMToRecentSearchTokenConverter
import com.tangem.features.feed.model.search.converter.MarketsListItemUMWithAppCurrency
import com.tangem.features.feed.model.search.converter.RecentSearchTokenToMarketsListItemUMConverter
import com.tangem.features.feed.model.search.converter.RecentSearchTokenWithAppCurrency
import com.tangem.features.feed.model.search.state.SearchStateController
import com.tangem.features.feed.model.search.state.transformers.*
import com.tangem.features.feed.ui.search.state.*
@ -28,6 +36,9 @@ import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -45,7 +56,9 @@ internal class SearchModel @Inject constructor(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getSearchResultsUseCase: GetSearchResultsUseCase,
private val saveSearchQueryUseCase: SaveSearchQueryUseCase,
private val saveRecentSearchTokenUseCase: SaveRecentSearchTokenUseCase,
private val clearSearchHistoryUseCase: ClearSearchHistoryUseCase,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val stateController: SearchStateController,
) : Model() {
@ -64,6 +77,18 @@ internal class SearchModel @Inject constructor(
initialValue = AppCurrency.Default,
)
private val marketsListItemToRecentSearchTokenConverter by lazy {
MarketsListItemUMToRecentSearchTokenConverter()
}
private val recentSearchTokenToMarketsListItemConverter by lazy {
RecentSearchTokenToMarketsListItemUMConverter()
}
private val priceAndTimePointValuesConverter by lazy {
PriceAndTimePointValuesConverter(shouldFormatAxis = false)
}
private val searchMarketsListManager by lazy {
MarketsListBatchFlowManager(
getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase,
@ -108,8 +133,15 @@ internal class SearchModel @Inject constructor(
stateController.update(UpdateSearchBarQueryTransformer(text))
}
fun onResultMarketTokenClick() {
fun onResultMarketTokenClick(item: MarketsListItemUM) {
modelScope.launch(dispatchers.default) {
val appCurrency = currentAppCurrency.value
val input = MarketsListItemUMWithAppCurrency(
item = item,
appCurrencyCode = appCurrency.code,
appCurrencySymbol = appCurrency.symbol,
)
saveRecentSearchTokenUseCase(marketsListItemToRecentSearchTokenConverter.convert(input))
saveSearchQueryUseCase(stateController.value.searchBar.query)
}
}
@ -263,31 +295,43 @@ internal class SearchModel @Inject constructor(
TextHintItemUM(text = hint.text)
}.toImmutableList()
val appCurrency = currentAppCurrency.value
val recentTokens = searchResult.recentTokens.map { token ->
token.toMarketsListItemUM()
recentSearchTokenToMarketsListItemConverter.convert(
RecentSearchTokenWithAppCurrency(token = token, appCurrency = appCurrency),
)
}.toImmutableList()
stateController.update(UpdateHistoryTransformer(textHints, recentTokens))
loadRecentTokenCharts(recentTokens, appCurrency)
}
}.saveIn(searchResultsJob)
}
private fun RecentSearchToken.toMarketsListItemUM(): MarketsListItemUM {
return MarketsListItemUM(
id = id,
name = name,
currencySymbol = symbol,
iconUrl = imageUrl,
ratingPosition = null,
marketCap = null,
price = MarketsListItemUM.Price(text = ""),
trendPercentText = "",
trendType = PriceChangeType.NEUTRAL,
chartData = null,
isUnder100kMarketCap = false,
stakingRate = null,
updateTimestamp = timestamp,
)
private suspend fun loadRecentTokenCharts(tokens: ImmutableList<MarketsListItemUM>, appCurrency: AppCurrency) {
coroutineScope {
tokens.map { token ->
async(dispatchers.io) {
val chart = getTokenPriceChartUseCase(
appCurrency = appCurrency,
interval = PriceChangeInterval.H24,
tokenId = token.id,
tokenSymbol = token.currencySymbol,
preview = true,
).getOrElse { return@async }
val chartData = priceAndTimePointValuesConverter.convert(
MarketChartData.Data(
y = chart.priceY.toImmutableList(),
x = chart.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
).sorted(),
)
stateController.update(UpdateRecentTokenChartTransformer(token.id, chartData))
}
}.awaitAll()
}
}
private fun AccountName.toDisplayString(): String {

View file

@ -0,0 +1,41 @@
package com.tangem.features.feed.model.search.converter
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.search.model.RecentSearchToken
import com.tangem.domain.search.model.TokenPriceChangeDirection
import com.tangem.utils.converter.Converter
internal class MarketsListItemUMToRecentSearchTokenConverter :
Converter<MarketsListItemUMWithAppCurrency, RecentSearchToken> {
override fun convert(value: MarketsListItemUMWithAppCurrency): RecentSearchToken {
val item = value.item
val direction = when (item.trendType) {
PriceChangeType.UP -> TokenPriceChangeDirection.UP
PriceChangeType.DOWN -> TokenPriceChangeDirection.DOWN
PriceChangeType.NEUTRAL -> TokenPriceChangeDirection.NEUTRAL
}
val stakingText = when (val rate = item.stakingRate) {
is TextReference.Str -> rate.value
is TextReference.StyledStr -> rate.value
else -> null
}
return RecentSearchToken(
id = item.id,
name = item.name,
symbol = item.currencySymbol,
imageUrl = item.iconUrl,
timestamp = item.updateTimestamp ?: System.currentTimeMillis(),
appCurrencyCode = value.appCurrencyCode,
appCurrencySymbol = value.appCurrencySymbol,
price = item.price.fiatPrice,
priceChangePercent = item.trendPercentText,
priceChangeDirection = direction,
marketCap = item.marketCap,
ratingPosition = item.ratingPosition,
isUnder100kMarketCap = item.isUnder100kMarketCap,
stakingRateText = stakingText,
)
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.feed.model.search.converter
import com.tangem.common.ui.markets.models.MarketsListItemUM
internal data class MarketsListItemUMWithAppCurrency(
val item: MarketsListItemUM,
val appCurrencyCode: String,
val appCurrencySymbol: String,
)

View file

@ -0,0 +1,55 @@
package com.tangem.features.feed.model.search.converter
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.price
import com.tangem.domain.search.model.TokenPriceChangeDirection
import com.tangem.utils.converter.Converter
internal class RecentSearchTokenToMarketsListItemUMConverter :
Converter<RecentSearchTokenWithAppCurrency, MarketsListItemUM> {
override fun convert(value: RecentSearchTokenWithAppCurrency): MarketsListItemUM {
val token = value.token
val currencyCode = token.appCurrencyCode.ifEmpty { value.appCurrency.code }
val currencySymbol = token.appCurrencySymbol.ifEmpty { value.appCurrency.symbol }
val trendType = when (token.priceChangeDirection) {
TokenPriceChangeDirection.UP -> PriceChangeType.UP
TokenPriceChangeDirection.DOWN -> PriceChangeType.DOWN
TokenPriceChangeDirection.NEUTRAL -> PriceChangeType.NEUTRAL
}
val priceText = token.price.format {
fiat(
fiatCurrencyCode = currencyCode,
fiatCurrencySymbol = currencySymbol,
).price()
}
return MarketsListItemUM(
id = token.id,
name = token.name,
currencySymbol = token.symbol,
iconUrl = token.imageUrl,
ratingPosition = token.ratingPosition,
marketCap = token.marketCap,
price = MarketsListItemUM.Price(
text = priceText,
annotated = token.price.toMarketsListItemPriceAnnotated(
appCurrencyCode = currencyCode,
appCurrencySymbol = currencySymbol,
),
changeType = null,
fiatPrice = token.price,
),
trendPercentText = token.priceChangePercent,
trendType = trendType,
chartData = null,
isUnder100kMarketCap = token.isUnder100kMarketCap,
stakingRate = token.stakingRateText?.let { TextReference.Str(it) },
updateTimestamp = token.timestamp,
)
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.feed.model.search.converter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.search.model.RecentSearchToken
internal data class RecentSearchTokenWithAppCurrency(
val token: RecentSearchToken,
val appCurrency: AppCurrency,
)

View file

@ -0,0 +1,30 @@
package com.tangem.features.feed.model.search.state.transformers
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.ui.search.state.SearchContentUM
import com.tangem.features.feed.ui.search.state.SearchUM
import kotlinx.collections.immutable.toImmutableList
internal class UpdateRecentTokenChartTransformer(
private val tokenId: CryptoCurrency.RawID,
private val chartData: MarketChartRawData,
) : SearchUMTransformer {
override fun transform(prevState: SearchUM): SearchUM {
val content = prevState.content
if (content !is SearchContentUM.History) return prevState
val updatedTokens = content.recentTokens.map { token ->
if (token.id == tokenId) {
token.copy(chartData = chartData)
} else {
token
}
}.toImmutableList()
return prevState.copy(
content = content.copy(recentTokens = updatedTokens),
)
}
}

View file

@ -24,6 +24,7 @@ import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.*
import com.tangem.domain.models.earn.EarnType
import com.tangem.features.feed.ui.earn.components.*
import com.tangem.features.feed.ui.earn.state.*
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
@ -641,7 +642,8 @@ private fun previewEarnListItemUM(
shouldShowCustomBadge = false,
),
earnValue = stringReference("APY 6.54%"),
earnType = stringReference("Yield"),
earnTypeTitle = stringReference("Yield"),
earnType = EarnType.YIELD,
onItemClick = {},
)

View file

@ -30,6 +30,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.*
import com.tangem.domain.models.earn.EarnType
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
@Composable
@ -95,7 +96,7 @@ private fun EarnListItemV1(item: EarnListItemUM, modifier: Modifier = Modifier)
)
SpacerH(2.dp)
Text(
text = item.earnType.resolveReference(),
text = item.earnTypeTitle.resolveReference(),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
maxLines = 1,
@ -147,6 +148,7 @@ private fun EarnListItemV2(item: EarnListItemUM, modifier: Modifier = Modifier)
ModeBlock(
modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM),
earnType = item.earnType,
earnTypeTitle = item.earnTypeTitle,
)
},
)
@ -200,19 +202,24 @@ private fun TokenTitle(name: String, symbol: String, modifier: Modifier = Modifi
}
@Composable
private fun ModeBlock(earnType: TextReference, modifier: Modifier = Modifier) {
private fun ModeBlock(earnType: EarnType, earnTypeTitle: TextReference, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_staking_new_16),
imageVector = ImageVector.vectorResource(
id = when (earnType) {
EarnType.STAKING -> R.drawable.ic_staking_new_16
EarnType.YIELD -> R.drawable.ic_yield_mode_16
},
),
tint = TangemTheme.colors2.markers.iconGray,
contentDescription = null,
)
Text(
text = earnType.resolveReference(),
text = earnTypeTitle.resolveReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.tertiary,
)
@ -239,7 +246,8 @@ private fun EarnListItemPreviewV1() {
shouldShowCustomBadge = false,
),
earnValue = stringReference("APY 8.50%"),
earnType = stringReference("Yield"),
earnTypeTitle = stringReference("Yield"),
earnType = EarnType.YIELD,
onItemClick = {},
),
)
@ -257,7 +265,8 @@ private fun EarnListItemPreviewV1() {
shouldShowCustomBadge = false,
),
earnValue = stringReference("APY 8.50%"),
earnType = stringReference("Yield"),
earnTypeTitle = stringReference("Yield"),
earnType = EarnType.YIELD,
onItemClick = {},
),
)
@ -286,7 +295,8 @@ private fun EarnListItemPreviewV2() {
shouldShowCustomBadge = false,
),
earnValue = stringReference("APY 8.50%"),
earnType = stringReference("Yield"),
earnTypeTitle = stringReference("Yield"),
earnType = EarnType.YIELD,
onItemClick = {},
),
)
@ -305,7 +315,8 @@ private fun EarnListItemPreviewV2() {
shouldShowCustomBadge = false,
),
earnValue = stringReference("APY 8.50%"),
earnType = stringReference("Yield"),
earnTypeTitle = stringReference("Yield"),
earnType = EarnType.YIELD,
onItemClick = {},
),
)

View file

@ -21,6 +21,7 @@ import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.*
import com.tangem.domain.models.earn.EarnType
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
@Composable
@ -161,7 +162,8 @@ private fun EarnListItemPreviewV1() {
shouldShowCustomBadge = false,
),
earnValue = stringReference("APY 6.54%"),
earnType = stringReference("Yield"),
earnTypeTitle = stringReference("Yield"),
earnType = EarnType.YIELD,
onItemClick = {},
),
onClick = {},
@ -187,7 +189,8 @@ private fun EarnListItemPreviewV2() {
shouldShowCustomBadge = false,
),
earnValue = stringReference("APY 6.54%"),
earnType = stringReference("Yield"),
earnTypeTitle = stringReference("Yield"),
earnType = EarnType.YIELD,
onItemClick = {},
),
onClick = {},

View file

@ -3,6 +3,7 @@ package com.tangem.features.feed.ui.earn.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.earn.EarnType
import kotlinx.collections.immutable.ImmutableList
@Immutable
@ -32,6 +33,7 @@ internal data class EarnListItemUM(
val tokenName: TextReference,
val currencyIconState: CurrencyIconState,
val earnValue: TextReference,
val earnType: TextReference,
val earnType: EarnType,
val earnTypeTitle: TextReference,
val onItemClick: () -> Unit,
)

View file

@ -11,12 +11,14 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.earn.EarnType
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
import com.tangem.features.feed.ui.earn.state.EarnListUM
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.features.feed.ui.feed.state.*
import kotlinx.collections.immutable.*
import java.math.BigDecimal
@Suppress("MagicNumber")
internal object FeedListPreviewDataProvider {
@ -246,7 +248,11 @@ internal object FeedListPreviewDataProvider {
iconUrl = null,
ratingPosition = rating,
marketCap = marketCap,
price = MarketsListItemUM.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(
text = "31 285.72$",
fiatPrice = BigDecimal(123123),
annotated = stringReference("31 285.72$"),
),
trendPercentText = percent,
trendType = trendType,
chartData = MarketChartRawData(
@ -273,7 +279,8 @@ internal object FeedListPreviewDataProvider {
shouldShowCustomBadge = false,
),
earnValue = stringReference("APY 6.54%"),
earnType = stringReference("Yield"),
earnTypeTitle = stringReference("Yield"),
earnType = EarnType.YIELD,
onItemClick = {},
)
}.toPersistentList()

View file

@ -37,6 +37,7 @@ import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.LocalRedesignEnabled
@ -182,6 +183,7 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
Column(modifier = Modifier.weight(1f)) {
TokenPriceText(
price = state.priceText,
priceAnnotated = state.priceAnnotated,
triggerPriceChange = state.triggerPriceChange,
)
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
@ -212,6 +214,28 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
@Composable
private fun TokenPriceText(
price: String,
triggerPriceChange: StateEvent<PriceChangeType>,
priceAnnotated: TextReference,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
TokenPriceTextV2(
priceAnnotated = priceAnnotated,
triggerPriceChange = triggerPriceChange,
modifier = modifier,
)
} else {
TokenPriceTextV1(
price = price,
triggerPriceChange = triggerPriceChange,
modifier = modifier,
)
}
}
@Composable
private fun TokenPriceTextV1(
price: String,
triggerPriceChange: StateEvent<PriceChangeType>,
modifier: Modifier = Modifier,
@ -244,6 +268,40 @@ private fun TokenPriceText(
)
}
@Composable
private fun TokenPriceTextV2(
priceAnnotated: TextReference,
triggerPriceChange: StateEvent<PriceChangeType>,
modifier: Modifier = Modifier,
) {
val growColor = TangemTheme.colors2.graphic.status.accent
val fallColor = TangemTheme.colors2.graphic.status.warning
val generalColor = TangemTheme.colors2.text.neutral.primary
val color = remember(generalColor) { Animatable(generalColor) }
EventEffect(triggerPriceChange) { priceChangeType ->
val nextColor = when (priceChangeType) {
PriceChangeType.UP,
-> growColor
PriceChangeType.DOWN -> fallColor
PriceChangeType.NEUTRAL -> return@EventEffect
}
color.animateTo(nextColor, snap())
color.animateTo(generalColor, tween(durationMillis = 500))
}
Text(
text = priceAnnotated.resolveAnnotatedReference(),
modifier = modifier,
color = color.value,
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography2.headingBold34.fontSize),
maxLines = 1,
style = TangemTheme.typography2.headingBold34,
)
}
@Composable
private fun IntervalSelector(
trendInterval: PriceChangeInterval,

View file

@ -156,12 +156,42 @@ internal fun FDVCard(item: InfoPointUMV2.FullyDilutedValuation) {
modifier = Modifier
.heightIn(120.dp)
.fillMaxWidth(),
title = { MetricValueText(value = item.value) },
title = {
if (item.fullyDilutedValuationChange24 != null) {
Row {
MetricValueText(value = item.fullyDilutedValuationChange24)
Text(
modifier = Modifier.padding(TangemTheme.dimens2.x1),
text = stringResourceSafe(R.string.markets_token_details_trading_interval),
style = TangemTheme.typography2.captionSemibold11,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
} else {
MetricValueText(value = item.value)
}
},
content = {
InformationTextBlock(
text = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
onInfoClick = item.onInfoClick,
)
Column {
if (item.fullyDilutedValuationChange24 != null) {
Text(
text = item.value?.resolveReference()
?: stringResourceSafe(R.string.token_market_metrics_no_data),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
SpacerH(4.dp)
}
InformationTextBlock(
text = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
onInfoClick = item.onInfoClick,
)
}
},
)
}
@ -445,6 +475,7 @@ private fun MetricsCardsPreview() {
FDVCard(
item = InfoPointUMV2.FullyDilutedValuation(
value = stringReference("$ 1.5 T"),
fullyDilutedValuationChange24 = stringReference("$ 2.44 M in total"),
onInfoClick = {},
),
)

View file

@ -50,6 +50,7 @@ internal object MarketsTokenDetailsPreview {
onScroll = {},
),
onShareClick = {},
priceAnnotated = stringReference("$0.00000000324"),
)
val contentState = MarketsTokenDetailsUM(
@ -144,5 +145,6 @@ internal object MarketsTokenDetailsPreview {
onScroll = {},
),
onShareClick = {},
priceAnnotated = stringReference("$0.00000000324"),
)
}

View file

@ -14,6 +14,7 @@ import java.math.BigDecimal
internal data class MarketsTokenDetailsUM(
val tokenName: String,
val priceText: String,
val priceAnnotated: TextReference,
val iconUrl: String?,
val dateTimeText: TextReference,
val priceChangePercentText: String?,

View file

@ -38,6 +38,7 @@ internal sealed interface InfoPointUMV2 {
@Immutable
data class FullyDilutedValuation(
val value: TextReference?,
val fullyDilutedValuationChange24: TextReference?,
val onInfoClick: () -> Unit,
) : InfoPointUMV2

View file

@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
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.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
@ -133,7 +134,10 @@ private fun LazyListScope.searchHistoryItems(
}
}
private fun LazyListScope.searchResultsItems(results: SearchContentUM.Results, onResultMarketTokenClick: () -> Unit) {
private fun LazyListScope.searchResultsItems(
results: SearchContentUM.Results,
onResultMarketTokenClick: (MarketsListItemUM) -> Unit,
) {
if (results.userAssets.isNotEmpty()) {
item(key = "header_portfolio") {
SectionHeader(title = stringResourceSafe(R.string.markets_search_portfolio_header))
@ -163,7 +167,7 @@ private fun LazyListScope.searchResultsItems(results: SearchContentUM.Results, o
private fun LazyListScope.marketSearchResultItems(
market: MarketSearchResultUM.Content,
hasUserAssetsSection: Boolean,
onResultMarketTokenClick: () -> Unit,
onResultMarketTokenClick: (MarketsListItemUM) -> Unit,
) {
if (hasUserAssetsSection) {
item(key = "spacer_between_sections") {
@ -185,7 +189,7 @@ private fun LazyListScope.marketSearchResultItems(
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
),
model = token,
onClick = onResultMarketTokenClick,
onClick = { onResultMarketTokenClick(token) },
)
}
if (market.shouldShowUnder100kNotification) {

View file

@ -25,6 +25,7 @@ import com.tangem.features.feed.ui.search.state.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
/** Labeled UI state for [SearchContent] previews. */
internal data class SearchContentPreviewScenario(
@ -192,7 +193,11 @@ internal object SearchContentPreviewFixtures {
iconUrl = null,
ratingPosition = rating,
marketCap = marketCap,
price = MarketsListItemUM.Price(text = priceText),
price = MarketsListItemUM.Price(
text = priceText,
annotated = stringReference(priceText),
fiatPrice = BigDecimal(123123),
),
trendPercentText = trendText,
trendType = trend,
chartData = chart,
@ -384,7 +389,7 @@ private val SearchContentPreviewCallbacks = SearchCallbacks(
onLoadMore = {},
onClearHintsClick = {},
onTextHintClick = { _ -> },
onResultMarketTokenClick = {},
onResultMarketTokenClick = { _ -> },
)
/** All [SearchContentPreviewScenario] values for the Preview Parameter dropdown in Android Studio. */

View file

@ -1,8 +1,10 @@
package com.tangem.features.feed.ui.search.state
import com.tangem.common.ui.markets.models.MarketsListItemUM
internal data class SearchCallbacks(
val onLoadMore: () -> Unit,
val onClearHintsClick: () -> Unit,
val onTextHintClick: (hint: String) -> Unit,
val onResultMarketTokenClick: () -> Unit,
val onResultMarketTokenClick: (MarketsListItemUM) -> Unit,
)

View file

@ -81,6 +81,7 @@ dependencies {
/* Common */
implementation(projects.common.ui)
implementation(projects.common.uiMarkets)
implementation(projects.common.uiCharts)
implementation(projects.common.routing)

View file

@ -6,6 +6,7 @@ import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.charts.state.MarketChartData
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
import com.tangem.common.ui.charts.state.sorted
import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -62,6 +63,8 @@ internal class TokenMarketBlockModel @Inject constructor(
TokenMarketBlockUM(
currencySymbol = params.cryptoCurrency.symbol,
currentPrice = null,
currentPriceValue = null,
priceAnnotated = null,
h24Percent = null,
priceChangeType = PriceChangeType.NEUTRAL,
chartData = null,
@ -86,15 +89,21 @@ internal class TokenMarketBlockModel @Inject constructor(
h24Percent = res.priceChange,
)
val appCurrency = currentAppCurrency.value
state.value = state.value.copy(
currentPrice = res.fiatRate.format {
fiat(
// TODO get currency from quotes use case [REDACTED_TASK_KEY]
fiatCurrencyCode = currentAppCurrency.value.code,
fiatCurrencyCode = appCurrency.code,
// TODO get currency from quotes use case [REDACTED_TASK_KEY]
fiatCurrencySymbol = currentAppCurrency.value.symbol,
fiatCurrencySymbol = appCurrency.symbol,
).price()
},
currentPriceValue = res.fiatRate,
priceAnnotated = res.fiatRate.toMarketsListItemPriceAnnotated(
appCurrencyCode = appCurrency.code,
appCurrencySymbol = appCurrency.symbol,
),
h24Percent = res.priceChange.format { percent() },
priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange),
)

View file

@ -20,6 +20,8 @@ import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.BlockCard
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.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -27,6 +29,7 @@ import com.tangem.features.markets.impl.R
import com.tangem.features.markets.token.block.impl.model.formatter.toChartType
import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
import kotlin.random.Random
@Composable
@ -47,6 +50,8 @@ internal fun TokenMarketBlockLegacy(state: TokenMarketBlockUM, modifier: Modifie
modifier = Modifier.weight(1f),
symbol = state.currencySymbol,
priceText = state.currentPrice,
priceValue = state.currentPriceValue,
priceAnnotated = state.priceAnnotated,
percentText = state.h24Percent,
type = state.priceChangeType,
)
@ -61,11 +66,14 @@ internal fun TokenMarketBlockLegacy(state: TokenMarketBlockUM, modifier: Modifie
)
}
@Suppress("LongParameterList")
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LeftSide(
symbol: String,
priceText: String?,
priceValue: BigDecimal?,
priceAnnotated: TextReference?,
percentText: String?,
type: PriceChangeType,
modifier: Modifier = Modifier,
@ -88,6 +96,8 @@ private fun LeftSide(
modifier = Modifier.alignByBaseline(),
price = priceText,
priceChangeType = type,
priceValue = priceValue ?: BigDecimal.ZERO,
priceAnnotated = priceAnnotated ?: TextReference.EMPTY,
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
@ -181,6 +191,8 @@ private fun Preview() {
priceChangeType = PriceChangeType.UP,
chartData = data,
onClick = {},
currentPriceValue = BigDecimal(1234),
priceAnnotated = stringReference("0,5$"),
)
TangemThemePreview {

View file

@ -2,10 +2,14 @@ package com.tangem.features.markets.token.block.impl.ui.state
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
import java.math.BigDecimal
internal data class TokenMarketBlockUM(
val currencySymbol: String,
val currentPrice: String?,
val currentPriceValue: BigDecimal?,
val priceAnnotated: TextReference?,
val h24Percent: String?,
val priceChangeType: PriceChangeType,
val chartData: MarketChartRawData?,

View file

@ -5,6 +5,7 @@ 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.common.ui.markets.toMarketsListItemPriceAnnotated
import com.tangem.core.ui.R
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.resourceReference
@ -119,7 +120,12 @@ internal class SwapMarketsTokenItemConverter(
return MarketsListItemUM.Price(
text = priceText,
annotated = tokenQuotesShort.currentPrice.toMarketsListItemPriceAnnotated(
appCurrencyCode = appCurrency.code,
appCurrencySymbol = appCurrency.symbol,
),
changeType = changeType,
fiatPrice = tokenQuotesShort.currentPrice,
)
}

View file

@ -5,6 +5,7 @@ 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.common.ui.markets.toMarketsListItemPriceAnnotated
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
@ -119,7 +120,12 @@ internal class SwapMarketsTokenItemConverter(
return MarketsListItemUM.Price(
text = priceText,
annotated = tokenQuotesShort.currentPrice.toMarketsListItemPriceAnnotated(
appCurrencyCode = appCurrency.code,
appCurrencySymbol = appCurrency.symbol,
),
changeType = changeType,
fiatPrice = tokenQuotesShort.currentPrice,
)
}

View file

@ -19,6 +19,7 @@ import com.tangem.feature.swap.models.market.state.SwapMarketState
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
internal object SwapSelectTokenPreviewProvider {
@ -205,7 +206,11 @@ internal object SwapSelectTokenPreviewProvider {
iconUrl = iconUrl,
ratingPosition = ratingPosition,
marketCap = marketCap,
price = MarketsListItemUM.Price(text = "31 285.72$"),
price = MarketsListItemUM.Price(
text = "31 285.72$",
annotated = stringReference("31 285.72$"),
fiatPrice = BigDecimal("123123"),
),
trendPercentText = "12.43%",
trendType = trendType,
chartData = chartData,