Updated on 2026-08-14
This commit is contained in:
parent
54800d32cc
commit
faa58cf142
39 changed files with 722 additions and 245 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue