Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-12 14:38:46 +03:00
parent 54800d32cc
commit faa58cf142
39 changed files with 722 additions and 245 deletions

View file

@ -30,10 +30,10 @@ fun TokenMarket.toSerializable(): TokenMarketSerializable {
symbol = symbol,
marketCap = marketCap,
tokenQuotes = TokenMarketSerializable.Quotes(
currentPrice = tokenQuotes.currentPrice,
h24Percent = tokenQuotes.h24Percent(),
weekPercent = tokenQuotes.weekPercent(),
monthPercent = tokenQuotes.monthPercent(),
currentPrice = tokenQuotesShort.currentPrice,
h24Percent = tokenQuotesShort.h24ChangePercent,
weekPercent = tokenQuotesShort.weekChangePercent,
monthPercent = tokenQuotesShort.monthChangePercent,
),
imageUrl = imageUrlLarge,
)

View file

@ -1,11 +1,9 @@
package com.tangem.features.markets.details.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -32,11 +30,23 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
LifecycleStartEffect(Unit) {
model.isVisibleOnScreen.value = true
onStopOrDispose {
model.isVisibleOnScreen.value = false
}
}
val state by model.state.collectAsStateWithLifecycle()
val bsState by bottomSheetState
LaunchedEffect(bsState) {
model.containerBottomSheetState.value = bsState
}
MarketsTokenDetailsContent(
state = state,
onBackClick = { onBack() },
onBackClick = onBack,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)

View file

@ -9,19 +9,22 @@ import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
import com.tangem.domain.markets.GetTokenPriceChartUseCase
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.*
import com.tangem.features.markets.component.BottomSheetState
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter
import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter
import com.tangem.features.markets.details.impl.model.formatter.*
import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice
import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween
import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.impl.R
@ -30,13 +33,15 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.joda.time.DateTime
import java.math.BigDecimal
import java.math.RoundingMode
import javax.inject.Inject
@Suppress("LargeClass")
@Suppress("LargeClass", "LongParameterList")
@Stable
internal class MarketsTokenDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
@ -44,10 +49,12 @@ internal class MarketsTokenDetailsModel @Inject constructor(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
private val getTokenQuotesUseCase: GetTokenQuotesUseCase,
private val urlOpener: UrlOpener,
) : Model() {
val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
private var quotesJob = JobHolder()
private val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
@ -77,13 +84,13 @@ internal class MarketsTokenDetailsModel @Inject constructor(
chartData = MarketChartData.NoData.Loading
updateLook {
val percentChangeType = params.token.tokenQuotes.h24Percent.percentChangeType()
it.copy(
type = getChartTypeByPercent(params.token.tokenQuotes.h24Percent),
xAxisFormatter = { value ->
value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter)
},
type = percentChangeType.toChartType(),
xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24),
yAxisFormatter = { value ->
BigDecimalFormatter.formatFiatAmountUncapped(
BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = value,
fiatCurrencyCode = currentAppCurrency.value.code,
fiatCurrencySymbol = "",
@ -93,10 +100,28 @@ internal class MarketsTokenDetailsModel @Inject constructor(
}
}
private val currentQuotes = MutableStateFlow(
TokenQuotes(
currentPrice = params.token.tokenQuotes.currentPrice,
h24ChangePercent = params.token.tokenQuotes.h24Percent,
weekChangePercent = params.token.tokenQuotes.weekPercent,
monthChangePercent = params.token.tokenQuotes.monthPercent,
m3ChangePercent = null,
m6ChangePercent = null,
yearChangePercent = null,
allTimeChangePercent = null,
),
)
private var lastUpdatedTimestamp: Long = DateTime.now().millis
val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED)
val isVisibleOnScreen = MutableStateFlow(false)
val state = MutableStateFlow(
MarketsTokenDetailsUM(
tokenName = params.token.name,
priceText = BigDecimalFormatter.formatFiatAmountUncapped(
priceText = BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = params.token.tokenQuotes.currentPrice,
fiatCurrencyCode = currentAppCurrency.value.code,
fiatCurrencySymbol = currentAppCurrency.value.symbol,
@ -121,7 +146,9 @@ internal class MarketsTokenDetailsModel @Inject constructor(
),
selectedInterval = PriceChangeInterval.H24,
onSelectedIntervalChange = ::onSelectedIntervalChange,
markerSet = false,
body = MarketsTokenDetailsUM.Body.Loading,
triggerPriceChange = consumedEvent(),
infoBottomSheet = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
@ -146,21 +173,22 @@ internal class MarketsTokenDetailsModel @Inject constructor(
}
private fun initialLoad() {
loadChart(state.value.selectedInterval)
loadInfo()
loadChart(state.value.selectedInterval)
modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS)
}
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
if (state.value.selectedInterval == interval) return
state.update {
it.copy(
selectedInterval = interval,
priceChangeType = PriceChangeType.UP,
private fun loadQuotes() {
modelScope.launch {
val result = getTokenQuotesUseCase(
tokenId = params.token.id,
appCurrency = currentAppCurrency.value,
)
}
loadChart(interval)
result.onRight { res ->
updateQuotes(res)
}
}
}
private fun loadChart(interval: PriceChangeInterval) {
@ -192,7 +220,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
)
}
val xAxisFormatter = getFormatterByInterval(state.value.selectedInterval)
val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval)
chart.onRight {
chartDataProducer.runTransactionSuspend {
@ -204,6 +232,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
updateLook {
it.copy(
xAxisFormatter = xAxisFormatter,
type = state.value.priceChangeType.toChartType(),
)
}
}
@ -247,8 +276,15 @@ internal class MarketsTokenDetailsModel @Inject constructor(
tokenMarketInfo.fold(
ifRight = { result ->
currentQuotes.value = result.quotes
val percent = result.quotes.getPercentByInterval(interval = state.value.selectedInterval)
state.update {
it.copy(
priceText = result.quotes.currentPrice.formatAsPrice(currentAppCurrency.value),
priceChangePercentText = result.quotes.getFormattedPercentByInterval(
interval = it.selectedInterval,
),
priceChangeType = percent.percentChangeType(),
body = MarketsTokenDetailsUM.Body.Content(
description = descriptionConverter.convert(result),
infoBlocks = infoConverter.convert(result),
@ -275,57 +311,101 @@ internal class MarketsTokenDetailsModel @Inject constructor(
}
}
private fun getFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
return when (interval) {
PriceChangeInterval.H24 -> { value: BigDecimal ->
value.toLong().toTimeFormat(DateTimeFormatters.timeFormatter)
}
PriceChangeInterval.WEEK,
PriceChangeInterval.MONTH,
PriceChangeInterval.MONTH3,
PriceChangeInterval.MONTH6,
-> { value ->
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd)
}
PriceChangeInterval.YEAR -> { value ->
value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd)
}
PriceChangeInterval.ALL_TIME -> { value ->
value.toLong().toTimeFormat(DateTimeFormatters.dateYYYY)
}
private suspend fun updateQuotes(newQuotes: TokenQuotes) {
val triggerPriceChangeType = getFormattedPriceChange(
currentPrice = currentQuotes.value.currentPrice,
updatedPrice = newQuotes.currentPrice,
)
val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) {
triggeredEvent(
data = triggerPriceChangeType,
onConsume = {
state.update { it.copy(triggerPriceChange = consumedEvent()) }
},
)
} else {
consumedEvent()
}
val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval)
val priceChangeType = percent.percentChangeType()
// wait until marker is removed
state.first { it.markerSet.not() }
currentQuotes.value = newQuotes
lastUpdatedTimestamp = DateTime.now().millis
state.update { stateToUpdate ->
stateToUpdate.copy(
priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency.value),
priceChangePercentText = newQuotes.getFormattedPercentByInterval(
interval = stateToUpdate.selectedInterval,
),
priceChangeType = priceChangeType,
triggerPriceChange = trigger,
dateTimeText = getDefaultDateTimeString(stateToUpdate.selectedInterval),
)
}
}
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
if (state.value.selectedInterval == interval) return
val quotes = currentQuotes.value
val priceChangePercent = quotes.getFormattedPercentByInterval(interval)
state.update {
it.copy(
priceChangePercentText = priceChangePercent,
selectedInterval = interval,
priceChangeType = quotes.getPercentByInterval(interval)?.percentChangeType()
?: PriceChangeType.NEUTRAL,
dateTimeText = getDefaultDateTimeString(interval),
)
}
loadChart(interval)
if (priceChangePercent.isEmpty()) {
loadQuotes()
}
}
@Suppress("MagicNumber")
private fun onMarkerPointSelected(time: BigDecimal?, price: BigDecimal?) {
val timeText = time?.toLong()?.toTimeFormat(DateTimeFormatters.dateTimeFormatter)?.let {
resourceReference(R.string.common_range, wrappedList(it, resourceReference(R.string.common_now)))
} ?: resourceReference(R.string.common_today)
private fun onMarkerPointSelected(markerTimestamp: BigDecimal?, price: BigDecimal?) {
val currentState = state.value
val percent = price?.subtract(params.token.tokenQuotes.currentPrice)
?.divide(params.token.tokenQuotes.currentPrice, 4, RoundingMode.HALF_UP)
?.multiply(BigDecimal(-100))
?: params.token.tokenQuotes.h24Percent
val dateTimeText = markerTimestamp?.let {
MarketsDateTimeFormatters.formatDateByIntervalWithMarker(
interval = currentState.selectedInterval,
markerTimestamp = it,
)
} ?: getDefaultDateTimeString(currentState.selectedInterval)
val percentText = BigDecimalFormatter.formatPercent(
percent = percent,
useAbsoluteValue = true,
)
val priceText = (price ?: currentQuotes.value.currentPrice).formatAsPrice(currentAppCurrency.value)
state.update {
it.copy(
dateTimeText = timeText,
priceText = BigDecimalFormatter.formatFiatAmountUncapped(
fiatAmount = price ?: params.token.tokenQuotes.currentPrice,
fiatCurrencyCode = currentAppCurrency.value.code,
fiatCurrencySymbol = currentAppCurrency.value.symbol,
),
val percent = price?.let {
getChangePercentBetween(
previousPrice = it,
currentPrice = currentQuotes.value.currentPrice,
)
} ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval)
val percentText = percent?.let {
BigDecimalFormatter.formatPercent(
percent = it,
useAbsoluteValue = true,
)
} ?: ""
state.update { stateToUpdate ->
stateToUpdate.copy(
markerSet = markerTimestamp != null,
dateTimeText = dateTimeText,
priceText = priceText,
priceChangePercentText = percentText,
priceChangeType = when {
percent < BigDecimal.ZERO -> PriceChangeType.DOWN
percent > BigDecimal.ZERO -> PriceChangeType.UP
else -> PriceChangeType.NEUTRAL
},
priceChangeType = percent.percentChangeType(),
)
}
@ -338,14 +418,6 @@ internal class MarketsTokenDetailsModel @Inject constructor(
}
}
private fun getChartTypeByPercent(percent: BigDecimal): MarketChartLook.Type {
return if (percent >= BigDecimal.ZERO) {
MarketChartLook.Type.Growing
} else {
MarketChartLook.Type.Falling
}
}
private fun showInfoBottomSheet(content: InfoBottomSheetContent) {
state.update { stateToUpdate ->
stateToUpdate.copy(
@ -379,6 +451,35 @@ internal class MarketsTokenDetailsModel @Inject constructor(
currentState.body is MarketsTokenDetailsUM.Body.Nothing
) {
loadInfo()
modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS)
}
}
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
launch {
while (true) {
delay(timeMillis)
// Update quotes only when the container bottom sheet is in the expanded state
containerBottomSheetState.first { it == BottomSheetState.EXPANDED }
// and is visible on the screen
isVisibleOnScreen.first { it }
loadQuotes()
}
}.saveIn(quotesJob)
}
private fun getDefaultDateTimeString(interval: PriceChangeInterval): TextReference {
return MarketsDateTimeFormatters.formatDateByInterval(
interval = interval,
startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval(
interval = interval,
currentTimestamp = lastUpdatedTimestamp,
),
)
}
private companion object {
const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L
}
}

View file

@ -42,8 +42,9 @@ internal class PricePerformanceConverter(
private fun BigDecimal?.convert(): String {
val currency = appCurrency()
return BigDecimalFormatter.formatCompactFiatAmount(
amount = this,
return BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = this,
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
)

View file

@ -0,0 +1,98 @@
package com.tangem.features.markets.details.impl.model.formatter
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenQuotes
import java.math.BigDecimal
import java.math.RoundingMode
internal fun BigDecimal.formatAsPrice(currency: AppCurrency): String {
return BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = this,
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
)
}
internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInterval): String {
val percent = when (interval) {
PriceChangeInterval.H24 -> h24ChangePercent
PriceChangeInterval.WEEK -> weekChangePercent
PriceChangeInterval.MONTH -> monthChangePercent
PriceChangeInterval.MONTH3 -> m3ChangePercent
PriceChangeInterval.MONTH6 -> m6ChangePercent
PriceChangeInterval.YEAR -> yearChangePercent
PriceChangeInterval.ALL_TIME -> allTimeChangePercent
}
return percent?.let {
BigDecimalFormatter.formatPercent(
percent = it,
useAbsoluteValue = true,
)
} ?: ""
}
internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? {
return when (interval) {
PriceChangeInterval.H24 -> h24ChangePercent
PriceChangeInterval.WEEK -> weekChangePercent
PriceChangeInterval.MONTH -> monthChangePercent
PriceChangeInterval.MONTH3 -> m3ChangePercent
PriceChangeInterval.MONTH6 -> m6ChangePercent
PriceChangeInterval.YEAR -> yearChangePercent
PriceChangeInterval.ALL_TIME -> allTimeChangePercent
}
}
internal fun BigDecimal?.percentChangeType(): PriceChangeType {
return when {
this == null -> PriceChangeType.NEUTRAL
this > BigDecimal.ZERO -> PriceChangeType.UP
this < BigDecimal.ZERO -> PriceChangeType.DOWN
else -> PriceChangeType.NEUTRAL
}
}
@Suppress("MagicNumber")
internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: BigDecimal): BigDecimal {
return if (previousPrice == BigDecimal.ZERO) {
BigDecimal.ZERO
} else {
currentPrice.subtract(previousPrice).divide(previousPrice, 4, RoundingMode.HALF_UP)
}
}
internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType {
val updatedPriceDecimals = BigDecimalFormatter.getProperFiatPriceDecimals(updatedPrice)
val current = currentPrice.setScale(updatedPriceDecimals, RoundingMode.HALF_UP)
val updated = updatedPrice.setScale(updatedPriceDecimals, RoundingMode.HALF_UP)
return when {
updated > current -> PriceChangeType.UP
updated < current -> PriceChangeType.DOWN
else -> PriceChangeType.NEUTRAL
}
}
internal fun PriceChangeType.toChartType(): MarketChartLook.Type {
return when (this) {
PriceChangeType.UP -> MarketChartLook.Type.Growing
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Growing
}
}
internal fun getChartTypeByPercent(percent: BigDecimal?): MarketChartLook.Type {
return when {
percent == null -> return MarketChartLook.Type.Growing
percent == BigDecimal.ZERO -> MarketChartLook.Type.Growing
percent > BigDecimal.ZERO -> MarketChartLook.Type.Growing
percent < BigDecimal.ZERO -> MarketChartLook.Type.Falling
else -> MarketChartLook.Type.Growing
}
}

View file

@ -0,0 +1,130 @@
package com.tangem.features.markets.details.impl.model.formatter
import com.tangem.core.ui.extensions.TextReference
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.utils.DateTimeFormatters
import com.tangem.core.ui.utils.formatAsDateTime
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.markets.impl.R
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import java.math.BigDecimal
internal object MarketsDateTimeFormatters {
private const val H24_MILLIS = 24L * 60 * 60 * 1000
private const val WEEK_MILLIS = 7L * H24_MILLIS
private val dateTimeMMMFormatter by lazy {
DateTimeFormatters.getBestFormatterBySkeleton("dd MMM hh:mm")
}
private val dateFormatter = DateTimeFormatters.dateDDMMYYYY
internal fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
return when (interval) {
PriceChangeInterval.H24 -> { value: BigDecimal ->
value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter)
}
PriceChangeInterval.WEEK,
PriceChangeInterval.MONTH,
PriceChangeInterval.MONTH3,
PriceChangeInterval.MONTH6,
-> { value ->
value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd)
}
PriceChangeInterval.YEAR -> { value ->
value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd)
}
PriceChangeInterval.ALL_TIME -> { value ->
value.toLong().formatAsDateTime(DateTimeFormatters.dateYYYY)
}
}
}
internal fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference {
return when (interval) {
PriceChangeInterval.H24 -> resourceReference(R.string.common_today)
PriceChangeInterval.WEEK,
PriceChangeInterval.MONTH,
PriceChangeInterval.MONTH3,
-> {
resourceReference(
R.string.common_range_with_space,
wrappedList(
stringReference(
startTimestamp.formatAsDateTime(MarketsDateTimeFormatters.dateTimeMMMFormatter),
),
resourceReference(R.string.common_now),
),
)
}
PriceChangeInterval.MONTH6,
PriceChangeInterval.YEAR,
-> {
resourceReference(
R.string.common_range_with_space,
wrappedList(
stringReference(
startTimestamp.formatAsDateTime(MarketsDateTimeFormatters.dateFormatter),
),
resourceReference(R.string.common_now),
),
)
}
PriceChangeInterval.ALL_TIME -> resourceReference(R.string.common_all)
}
}
internal fun formatDateByIntervalWithMarker(
interval: PriceChangeInterval,
markerTimestamp: BigDecimal,
): TextReference {
return when (interval) {
PriceChangeInterval.H24,
PriceChangeInterval.WEEK,
PriceChangeInterval.MONTH,
PriceChangeInterval.MONTH3,
-> {
resourceReference(
R.string.common_range_with_space,
wrappedList(
stringReference(
markerTimestamp.toLong().formatAsDateTime(MarketsDateTimeFormatters.dateTimeMMMFormatter),
),
resourceReference(R.string.common_now),
),
)
}
PriceChangeInterval.MONTH6,
PriceChangeInterval.YEAR,
PriceChangeInterval.ALL_TIME,
-> {
resourceReference(
R.string.common_range_with_space,
wrappedList(
stringReference(
markerTimestamp.toLong().formatAsDateTime(MarketsDateTimeFormatters.dateFormatter),
),
resourceReference(R.string.common_now),
),
)
}
}
}
@Suppress("MagicNumber")
fun getStartTimestampByInterval(interval: PriceChangeInterval, currentTimestamp: Long): Long {
return when (interval) {
PriceChangeInterval.H24 -> currentTimestamp - H24_MILLIS
PriceChangeInterval.WEEK -> currentTimestamp - WEEK_MILLIS
PriceChangeInterval.MONTH -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(1).millis
PriceChangeInterval.MONTH3 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(3).millis
PriceChangeInterval.MONTH6 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(6).millis
PriceChangeInterval.YEAR -> DateTime(currentTimestamp, DateTimeZone.UTC).minusYears(1).millis
PriceChangeInterval.ALL_TIME -> 0
}
}
}

View file

@ -1,10 +1,13 @@
package com.tangem.features.markets.details.impl.ui
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@ -23,6 +26,9 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
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.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
@ -134,11 +140,12 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column {
Text(
text = state.priceText,
style = TangemTheme.typography.head,
color = TangemTheme.colors.text.primary1,
Column(
Modifier.weight(1f),
) {
TokenPriceText(
price = state.priceText,
triggerPriceChange = state.triggerPriceChange,
)
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
Text(
@ -155,7 +162,7 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
}
SpacerW4()
CoinIcon(
modifier = Modifier.size(TangemTheme.dimens.size48),
modifier = Modifier.requiredSize(TangemTheme.dimens.size48),
url = state.iconUrl,
alpha = 1f,
colorFilter = null,
@ -164,6 +171,38 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
}
}
@Composable
private fun TokenPriceText(
price: String,
triggerPriceChange: StateEvent<PriceChangeType>,
modifier: Modifier = Modifier,
) {
val growColor = TangemTheme.colors.text.accent
val fallColor = TangemTheme.colors.text.warning
val generalColor = TangemTheme.colors.text.primary1
val color = remember { Animatable(generalColor) }
EventEffect(triggerPriceChange) {
val nextColor = when (it) {
PriceChangeType.UP,
-> growColor
PriceChangeType.DOWN -> fallColor
PriceChangeType.NEUTRAL -> return@EventEffect
}
color.animateTo(nextColor, snap())
color.animateTo(generalColor, tween(durationMillis = 500))
}
Text(
modifier = modifier,
text = price,
color = color.value,
style = TangemTheme.typography.head,
)
}
@Composable
private fun IntervalSelector(
trendInterval: PriceChangeInterval,
@ -224,9 +263,9 @@ private fun Preview() {
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
state = MarketsTokenDetailsUM(
tokenName = "Token Name",
priceText = "Price",
dateTimeText = stringReference("Date Time"),
priceChangePercentText = "Price Change",
priceText = "$0.00000000324",
dateTimeText = stringReference("Today"),
priceChangePercentText = "52.00%",
iconUrl = "",
priceChangeType = PriceChangeType.UP,
chartState = MarketsTokenDetailsUM.ChartState(
@ -244,6 +283,8 @@ private fun Preview() {
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
markerSet = false,
triggerPriceChange = consumedEvent(),
),
onHeaderSizeChange = {},
onBackClick = {},

View file

@ -1,7 +1,6 @@
package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text

View file

@ -5,6 +5,7 @@ import com.tangem.common.ui.charts.state.MarketChartDataProducer
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.markets.PriceChangeInterval
import java.math.BigDecimal
@ -17,9 +18,11 @@ internal data class MarketsTokenDetailsUM(
val priceChangePercentText: String,
val priceChangeType: PriceChangeType,
val selectedInterval: PriceChangeInterval,
val markerSet: Boolean,
val chartState: ChartState,
val onSelectedIntervalChange: (PriceChangeInterval) -> Unit,
val infoBottomSheet: TangemBottomSheetConfig,
val triggerPriceChange: StateEvent<PriceChangeType>,
val body: Body,
) {

View file

@ -48,17 +48,17 @@ internal class MarketsTokenItemConverter(
ratingPosition = new.marketRating?.toString(),
marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() },
iconUrl = new.imageUrlLarge,
price = ifChanged(prev = prev.tokenQuotes, new = new.tokenQuotes, prevR = prevUI.price) {
price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) {
new.getCurrentPrice(
prev = prev,
)
},
trendPercentText = ifChanged(
prev.tokenQuotes,
new.tokenQuotes,
prev.tokenQuotesShort,
new.tokenQuotesShort,
prevUI.trendPercentText,
) { new.getTrendPercent() },
trendType = ifChanged(prev.tokenQuotes, new.tokenQuotes, prevUI.trendType) { new.getTrendType() },
trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() },
chardData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chardData) { new.getChartData() },
)
}
@ -79,16 +79,16 @@ internal class MarketsTokenItemConverter(
}
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
val prevPrice = prev?.tokenQuotes?.currentPrice
val prevPrice = prev?.tokenQuotesShort?.currentPrice
val priceText = BigDecimalFormatter.formatFiatAmountUncapped(
fiatAmount = tokenQuotes.currentPrice,
val priceText = BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = tokenQuotesShort.currentPrice,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
val changeType = if (prevPrice != null) {
if (tokenQuotes.currentPrice > prevPrice) {
if (tokenQuotesShort.currentPrice > prevPrice) {
PriceChangeType.UP
} else {
PriceChangeType.DOWN
@ -122,9 +122,9 @@ internal class MarketsTokenItemConverter(
private fun TokenMarket.getTrendType(): PriceChangeType {
val percent = when (currentTrendInterval) {
TrendInterval.H24 -> tokenQuotes.h24Percent()
TrendInterval.D7 -> tokenQuotes.weekPercent()
TrendInterval.M1 -> tokenQuotes.monthPercent()
TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent
TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
}.setScale(2, RoundingMode.UP)
return when (percent.compareTo(BigDecimal.ZERO)) {
@ -136,9 +136,9 @@ internal class MarketsTokenItemConverter(
private fun TokenMarket.getTrendPercent(): String {
val percent = when (currentTrendInterval) {
TrendInterval.H24 -> tokenQuotes.h24Percent()
TrendInterval.D7 -> tokenQuotes.weekPercent()
TrendInterval.M1 -> tokenQuotes.monthPercent()
TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent
TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
}
return BigDecimalFormatter.formatPercent(

View file

@ -4,7 +4,6 @@ import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.staking.impl.R
internal sealed class StakingNotification(val config: NotificationConfig) {
@ -55,8 +54,7 @@ internal sealed class StakingNotification(val config: NotificationConfig) {
) : Warning(
title = resourceReference(R.string.staking_notification_earn_rewards_title),
subtitle = resourceReference(
R.string.staking_notification_earn_rewards_text,
wrappedList(currencyName, days),
R.string.common_error, // FIXME
),
)
}