Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-28 15:46:27 +03:00
parent 297a006e3d
commit 7b7cece46f
35 changed files with 436 additions and 422 deletions

View file

@ -41,7 +41,7 @@ object MarketsDomainModule {
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetTokenQuotesUseCase {
return GetTokenQuotesUseCase(quotesRepository = quotesRepository)
fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(quotesRepository = quotesRepository)
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.common.ui.tokens
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.snap
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.style.TextOverflow
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.res.TangemTheme
/**
* Text view for token price.
*
* @param price Price of the token.
* @param priceChangeType Type of the price change.
*/
@Composable
fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
val growColor = TangemTheme.colors.text.accent
val fallColor = TangemTheme.colors.text.warning
val generalColor = TangemTheme.colors.text.primary1
val color = remember(generalColor) { Animatable(generalColor) }
var animationSkipped by remember { mutableStateOf(false) }
LaunchedEffect(price) {
if (animationSkipped.not()) {
animationSkipped = true
return@LaunchedEffect
}
if (priceChangeType != null) {
val nextColor = when (priceChangeType) {
PriceChangeType.UP,
-> growColor
PriceChangeType.DOWN -> fallColor
PriceChangeType.NEUTRAL -> return@LaunchedEffect
}
color.animateTo(nextColor, snap())
color.animateTo(generalColor, tween(durationMillis = 500))
}
}
Text(
modifier = modifier,
text = price,
color = color.value,
maxLines = 1,
style = TangemTheme.typography.body2,
overflow = TextOverflow.Visible,
)
}

View file

@ -67,7 +67,16 @@ data class TokenMarketInfoResponse(
val buyPressureChange: Change?,
@Json(name = "experienced_buyer_change")
val experiencedBuyerChange: Change?,
)
@Json(name = "networks")
val sourceNetworks: List<SourceNetwork>?,
) {
data class SourceNetwork(
@Json(name = "network_id")
val id: String,
@Json(name = "network_name")
val name: String,
)
}
data class Change(
@Json(name = "24h")

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
@ -69,6 +70,7 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie
textStyle = TangemTheme.typography.body2.copy(
color = TangemTheme.colors.text.primary1,
),
cursorBrush = SolidColor(TangemTheme.colors.icon.primary1),
decorationBox = @Composable { innerTextField ->
DecorationBox(
state = state,

View file

@ -336,6 +336,15 @@ object BigDecimalFormatter {
): String {
if (amount == null) return EMPTY_BALANCE_SIGN
if (amount < BigDecimal.ONE) {
return formatFiatPriceUncapped(
fiatAmount = amount,
fiatCurrencyCode = fiatCurrencyCode,
fiatCurrencySymbol = fiatCurrencySymbol,
locale = locale,
)
}
val rawAmount = formatCompactAmount(
amount = amount,
locale = locale,

View file

@ -9,4 +9,5 @@ object StringsSigns {
const val DASH_SIGN = ""
const val LOWER_SIGN = "<"
const val TILDE_SIGN = "~"
const val NON_BREAKING_SPACE = '\u00A0'
}

View file

@ -7,8 +7,10 @@ import kotlinx.coroutines.yield
import timber.log.Timber
import kotlin.coroutines.cancellation.CancellationException
@Suppress("UnconditionalJumpStatementInLoop")
suspend fun <T> retryOnError(priority: Boolean = false, call: suspend () -> T): T {
@Suppress("UnconditionalJumpStatementInLoop", "MagicNumber")
suspend fun <T> retryOnError(priority: Boolean = false, startRetryDelay: Int = 500, call: suspend () -> T): T {
var currentDelay = startRetryDelay
var priorityCounter = 5
while (true) {
return try {
call()
@ -19,9 +21,12 @@ suspend fun <T> retryOnError(priority: Boolean = false, call: suspend () -> T):
Timber.e(e, "Error occurred during retryOnError block")
if (priority.not()) {
if (priority && priorityCounter > 0) {
--priorityCounter
} else {
yield()
delay(timeMillis = 500)
delay(timeMillis = currentDelay.toLong())
currentDelay *= 2
}
continue

View file

@ -57,6 +57,7 @@ internal object TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, To
liquidityChange = liquidityChange?.convert(),
buyPressureChange = buyPressureChange?.convert(),
experiencedBuyerChange = experiencedBuyerChange?.convert(),
sourceNetworks = sourceNetworks.orEmpty().map { TokenMarketInfo.Insights.SourceNetwork(it.id, it.name) },
)
}

View file

@ -37,14 +37,14 @@ internal class DefaultQuotesRepository(
private val mutex = Mutex()
@OptIn(ExperimentalCoroutinesApi::class)
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> {
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Flow<Set<Quote>> {
return appPreferencesStore.getObject<CurrenciesResponse.Currency>(
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
)
.distinctUntilChanged()
.filterNotNull()
.flatMapLatest { appCurrency ->
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false)
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = refresh)
quotesStore.get(currenciesIds).map(quotesConverter::convertSet)
}
.cancellable()

View file

@ -27,7 +27,13 @@ data class TokenMarketInfo(
val liquidityChange: Change?,
val buyPressureChange: Change?,
val experiencedBuyerChange: Change?,
)
val sourceNetworks: List<SourceNetwork>,
) {
data class SourceNetwork(
val id: String,
val name: String,
)
}
data class Change(
val day: BigDecimal?,

View file

@ -0,0 +1,24 @@
package com.tangem.domain.markets
import arrow.core.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.QuotesRepository
import kotlinx.coroutines.flow.*
@Suppress("UnusedPrivateMember")
class GetCurrencyQuotesUseCase(
private val quotesRepository: QuotesRepository,
) {
// TODO apply interval parameter [REDACTED_TASK_KEY]
operator fun invoke(
currencyID: CryptoCurrency.ID,
interval: PriceChangeInterval,
refresh: Boolean,
): Flow<Option<Quote>> {
return quotesRepository.getQuotesUpdates(
currenciesIds = setOf(currencyID),
refresh = refresh,
).map { it.firstOrNull().toOption() }.catch { emit(None) }
}
}

View file

@ -1,26 +0,0 @@
package com.tangem.domain.markets
import arrow.core.Either
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.QuotesRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@Suppress("UnusedPrivateMember")
class GetTokenQuotesUseCase(
private val quotesRepository: QuotesRepository,
) {
operator fun invoke(tokenId: String, interval: PriceChangeInterval): Flow<Either<Unit, Quote>> {
return flowOf(
Either.catch {
Quote(
rawCurrencyId = "USD",
fiatRate = 100.toBigDecimal(), // mock
priceChange = 10.toBigDecimal(), // mock
)
}.mapLeft {},
)
// TODO implement quotes fetching from repository [REDACTED_TASK_KEY]
// quotesRepository.getQuotesUpdates()
}
}

View file

@ -12,12 +12,12 @@ interface QuotesRepository {
/**
* Retrieves updates of quotes for a set of specified cryptocurrencies, identified by their unique IDs.
*
* Loads remote quotes if they have expired.
* Loads remote quotes if they have expired or if [refresh] is `true`.
*
* @param currenciesIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved.
* @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies.
*/
fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>>
fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean = false): Flow<Set<Quote>>
/**
* Retrieves quotes for a set of specified cryptocurrencies, identified by their unique IDs.

View file

@ -13,7 +13,7 @@ internal class MockQuotesRepository(
private val quotes: Flow<Either<DataError, Set<Quote>>>,
) : QuotesRepository {
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> {
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Flow<Set<Quote>> {
return quotes.map { it.getOrElse { e -> throw e } }
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.markets.token.block
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.tokens.model.CryptoCurrency
import kotlinx.serialization.Serializable
@Stable
@ -10,6 +11,7 @@ interface TokenMarketBlockComponent : ComposableContentComponent {
@Serializable
data class Params(
val cryptoCurrencyID: CryptoCurrency.ID,
val tokenId: String,
val tokenName: String,
val tokenSymbol: String,

View file

@ -13,7 +13,6 @@ 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.utils.BigDecimalFormatter
@ -24,6 +23,10 @@ import com.tangem.features.markets.details.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.model.state.QuotesStateUpdater
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
@ -76,6 +79,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
urlOpener.openUrl(it.url)
},
)
private val descriptionConverter = DescriptionConverter(
onReadModeClicked = {
showInfoBottomSheet(it)
@ -95,7 +99,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = value,
fiatCurrencyCode = currentAppCurrency.value.code,
fiatCurrencySymbol = "",
fiatCurrencySymbol = currentAppCurrency.value.symbol,
)
},
)
@ -115,7 +119,8 @@ internal class MarketsTokenDetailsModel @Inject constructor(
),
)
private var lastUpdatedTimestamp: Long = DateTime.now().millis
private val currentTokenInfo = MutableStateFlow<TokenMarketInfo?>(null)
private val lastUpdatedTimestamp = MutableStateFlow(DateTime.now().millis)
val isVisibleOnScreen = MutableStateFlow(false)
val networksState = MutableStateFlow<TokenNetworksState>(TokenNetworksState.Loading)
@ -155,6 +160,14 @@ internal class MarketsTokenDetailsModel @Inject constructor(
),
)
private val quotesStateUpdater = QuotesStateUpdater(
currentAppCurrency = Provider { currentAppCurrency.value },
state = state,
currentQuotes = currentQuotes,
lastUpdatedTimestamp = lastUpdatedTimestamp,
currentTokenInfo = currentTokenInfo,
)
private val loadChartJobHolder = JobHolder()
init {
@ -279,41 +292,7 @@ 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),
),
)
}
val networks = result.networks?.filter {
BlockchainUtils.isSupportedNetworkId(it.networkId)
}
networksState.value = if (networks.isNullOrEmpty()) {
TokenNetworksState.NoNetworksAvailable
} else {
TokenNetworksState.NetworksAvailable(networks)
}
chartDataProducer.runTransaction {
updateLook {
it.copy(
type = getChartTypeByPercent(percent),
)
}
}
},
ifRight = { result -> updateInfo(result) },
ifLeft = {
state.update {
if (it.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) {
@ -333,42 +312,50 @@ internal class MarketsTokenDetailsModel @Inject constructor(
}
}
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()) }
},
private fun updateInfo(newInfo: TokenMarketInfo) {
lastUpdatedTimestamp.value = DateTime.now().millis
currentTokenInfo.value = newInfo
currentQuotes.value = newInfo.quotes
val percent = newInfo.quotes.getPercentByInterval(interval = state.value.selectedInterval)
state.update {
it.copy(
priceText = newInfo.quotes.currentPrice.formatAsPrice(currentAppCurrency.value),
priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval(
interval = it.selectedInterval,
),
priceChangeType = percent.percentChangeType(),
body = MarketsTokenDetailsUM.Body.Content(
description = descriptionConverter.convert(newInfo),
infoBlocks = infoConverter.convert(newInfo),
),
)
} else {
consumedEvent()
}
val networks = newInfo.networks?.filter {
BlockchainUtils.isSupportedNetworkId(it.networkId)
}
networksState.value = if (networks.isNullOrEmpty()) {
TokenNetworksState.NoNetworksAvailable
} else {
TokenNetworksState.NetworksAvailable(networks)
}
chartDataProducer.runTransaction {
updateLook {
it.copy(
type = getChartTypeByPercent(percent),
)
}
}
}
private suspend fun updateQuotes(newQuotes: TokenQuotes) {
quotesStateUpdater.updateQuotes(newQuotes)
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),
)
}
chartDataProducer.runTransaction {
updateLook {
@ -489,9 +476,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
launch {
while (true) {
delay(timeMillis)
// Update quotes only when the container bottom sheet is in the expanded state
// and is visible on the screen
// Update quotes only when content visible on the screen
isVisibleOnScreen.first { it }
loadQuotes()
@ -504,7 +489,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
interval = interval,
startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval(
interval = interval,
currentTimestamp = lastUpdatedTimestamp,
currentTimestamp = lastUpdatedTimestamp.value,
),
)
}

View file

@ -2,7 +2,7 @@ package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
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.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketInfo
@ -14,7 +14,7 @@ import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
@Stable
@ -48,7 +48,10 @@ internal class InsightsConverter(
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_insights),
body = stringReference("//TODO"),
body = resourceReference(
R.string.markets_insights_info_description_message,
wrappedList(value.sourceNetworks.joinToString { it.name }),
),
),
)
},
@ -62,74 +65,79 @@ internal class InsightsConverter(
liquidityChange: BigDecimal?,
buyPressureChange: BigDecimal?,
): ImmutableList<InfoPointUM> {
return persistentListOf(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
value = experiencedBuyerChange.convertChange(),
change = experiencedBuyerChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
body = resourceReference(R.string.markets_token_details_experienced_buyers_description),
),
)
},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_buy_pressure),
value = buyPressureChange.convertChange(isFiatValue = true),
change = buyPressureChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_buy_pressure),
body = resourceReference(R.string.markets_token_details_buy_pressure_description),
),
)
},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_holders),
value = holdersChange.convertChange(),
change = holdersChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_holders),
body = resourceReference(R.string.markets_token_details_holders_description),
),
)
},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_liquidity),
value = liquidityChange.convertChange(),
change = liquidityChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_liquidity),
body = resourceReference(R.string.markets_token_details_liquidity_description),
),
)
},
),
)
return listOfNotNull(
experiencedBuyerChange?.let {
InfoPointUM(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
value = experiencedBuyerChange.convertChange(),
change = experiencedBuyerChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
body = resourceReference(R.string.markets_token_details_experienced_buyers_description),
),
)
},
)
},
buyPressureChange?.let {
InfoPointUM(
title = resourceReference(R.string.markets_token_details_buy_pressure),
value = buyPressureChange.convertChange(isFiatValue = true),
change = buyPressureChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_buy_pressure),
body = resourceReference(R.string.markets_token_details_buy_pressure_description),
),
)
},
)
},
holdersChange?.let {
InfoPointUM(
title = resourceReference(R.string.markets_token_details_holders),
value = holdersChange.convertChange(),
change = holdersChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_holders),
body = resourceReference(R.string.markets_token_details_holders_description),
),
)
},
)
},
liquidityChange?.let {
InfoPointUM(
title = resourceReference(R.string.markets_token_details_liquidity),
value = liquidityChange.convertChange(),
change = liquidityChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_liquidity),
body = resourceReference(R.string.markets_token_details_liquidity_description),
),
)
},
)
},
).toImmutableList()
}
private fun BigDecimal?.changeType(): InfoPointUM.ChangeType? {
private fun BigDecimal.changeType(): InfoPointUM.ChangeType? {
return when {
this == null -> null
this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP
this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN
else -> null
}
}
private fun BigDecimal?.convertChange(isFiatValue: Boolean = false): String {
if (this == null) return StringsSigns.DASH_SIGN
private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String {
val value = if (isFiatValue) {
val currency = appCurrency()
BigDecimalFormatter.formatCompactFiatAmount(
@ -141,11 +149,9 @@ internal class InsightsConverter(
BigDecimalFormatter.formatCompactAmount(amount = this.abs())
}
val spacing = if (isFiatValue) " " else ""
return when {
this > BigDecimal.ZERO -> StringsSigns.PLUS + spacing + value
this < BigDecimal.ZERO -> StringsSigns.MINUS + spacing + value
this > BigDecimal.ZERO -> StringsSigns.PLUS + value
this < BigDecimal.ZERO -> StringsSigns.MINUS + value
this == BigDecimal.ZERO -> value
else -> StringsSigns.DASH_SIGN
}

View file

@ -14,9 +14,6 @@ import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.NumberFormat
import java.util.Locale
@Stable
internal class MetricsConverter(
@ -116,19 +113,15 @@ internal class MetricsConverter(
private fun BigDecimal?.formatAmount(crypto: Boolean = false): String {
return if (crypto) {
val formatter = NumberFormat.getNumberInstance(Locale.getDefault()).apply {
maximumFractionDigits = 0
isGroupingUsed = true
roundingMode = RoundingMode.HALF_UP
}
formatter.format(this)
BigDecimalFormatter.formatCompactAmount(
amount = this ?: BigDecimal.ZERO,
)
} else {
val currency = appCurrency()
BigDecimalFormatter.formatFiatAmount(
fiatAmount = this,
BigDecimalFormatter.formatCompactFiatAmount(
amount = this,
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
decimals = 0,
)
}
}

View file

@ -7,24 +7,23 @@ import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
import java.math.RoundingMode
@Stable
internal class PricePerformanceConverter(
private val appCurrency: Provider<AppCurrency>,
) : Converter<TokenMarketInfo.PricePerformance, PricePerformanceUM> {
) {
override fun convert(value: TokenMarketInfo.PricePerformance): PricePerformanceUM {
fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM {
return PricePerformanceUM(
h24 = value.day.convert(),
month = value.month.convert(),
all = value.allTime.convert(),
h24 = value.day.convert(currentPrice),
month = value.month.convert(currentPrice),
all = value.allTime.convert(currentPrice),
)
}
private fun TokenMarketInfo.Range?.convert(): PricePerformanceUM.Value {
private fun TokenMarketInfo.Range?.convert(currentPrice: BigDecimal): PricePerformanceUM.Value {
if (this == null) {
return PricePerformanceUM.Value(
low = StringsSigns.DASH_SIGN,
@ -36,7 +35,7 @@ internal class PricePerformanceConverter(
return PricePerformanceUM.Value(
low = low.convert(),
high = high.convert(),
indicatorFraction = calculateFraction(),
indicatorFraction = calculateFraction(currentPrice),
)
}
@ -50,10 +49,15 @@ internal class PricePerformanceConverter(
)
}
private fun TokenMarketInfo.Range.calculateFraction(): Float {
if (low == null || high == null || low == BigDecimal.ZERO) return 0f
return (high!! - low!!).divide(low!!, RoundingMode.HALF_UP)
.setScale(2, RoundingMode.HALF_UP)
.toFloat().coerceAtMost(1f)
private fun TokenMarketInfo.Range.calculateFraction(currentPrice: BigDecimal): Float {
return when {
low == null || high == null || high == BigDecimal.ZERO || currentPrice < low -> 0f
currentPrice > high || low == high -> 1f
else -> {
(currentPrice - low!!).divide(high!! - low!!, RoundingMode.HALF_UP)
.setScale(2, RoundingMode.HALF_UP)
.toFloat().coerceAtMost(1f)
}
}
}
}

View file

@ -17,6 +17,9 @@ internal class TokenMarketInfoConverter(
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.InformationBlocks> {
private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
@Suppress("UnusedPrivateMember")
// TODO second markets iteration
private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick)
private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
private val pricePerformanceConverter = PricePerformanceConverter(appCurrency = appCurrency)
@ -25,9 +28,14 @@ internal class TokenMarketInfoConverter(
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks {
return MarketsTokenDetailsUM.InformationBlocks(
insights = value.insights?.let { insightsConverter.convert(it) },
securityScore = securityScoreConverter.convert(Unit),
securityScore = null,
metrics = value.metrics?.let { metricsConverter.convert(it) },
pricePerformance = value.pricePerformance?.let { pricePerformanceConverter.convert(it) },
pricePerformance = value.pricePerformance?.let {
pricePerformanceConverter.convert(
value = it,
currentPrice = value.quotes.currentPrice,
)
},
links = value.links?.let { linksConverter.convert(it) },
)
}

View file

@ -23,7 +23,7 @@ internal object MarketsDateTimeFormatters {
private val dateFormatter = DateTimeFormatters.dateDDMMYYYY
internal fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
return when (interval) {
PriceChangeInterval.H24 -> { value: BigDecimal ->
value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter)
@ -44,7 +44,7 @@ internal object MarketsDateTimeFormatters {
}
}
internal fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference {
fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference {
return when (interval) {
PriceChangeInterval.H24 -> resourceReference(R.string.common_today)
PriceChangeInterval.WEEK,
@ -78,10 +78,7 @@ internal object MarketsDateTimeFormatters {
}
}
internal fun formatDateByIntervalWithMarker(
interval: PriceChangeInterval,
markerTimestamp: BigDecimal,
): TextReference {
fun formatDateByIntervalWithMarker(interval: PriceChangeInterval, markerTimestamp: BigDecimal): TextReference {
return when (interval) {
PriceChangeInterval.H24,
PriceChangeInterval.WEEK,
@ -127,4 +124,14 @@ internal object MarketsDateTimeFormatters {
PriceChangeInterval.ALL_TIME -> 0
}
}
fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference {
return formatDateByInterval(
interval = interval,
startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval(
interval = interval,
currentTimestamp = currentTimestamp,
),
)
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.features.markets.details.impl.model.state
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.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenQuotes
import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter
import com.tangem.features.markets.details.impl.model.formatter.*
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.utils.Provider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import org.joda.time.DateTime
import java.math.BigDecimal
internal class QuotesStateUpdater(
private val currentAppCurrency: Provider<AppCurrency>,
private val state: MutableStateFlow<MarketsTokenDetailsUM>,
private val currentQuotes: MutableStateFlow<TokenQuotes>,
private val lastUpdatedTimestamp: MutableStateFlow<Long>,
private val currentTokenInfo: MutableStateFlow<TokenMarketInfo?>,
) {
private val pricePerformanceConverter = PricePerformanceConverter(currentAppCurrency)
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.value = DateTime.now().millis
state.update { stateToUpdate ->
stateToUpdate.copy(
priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency()),
priceChangePercentText = newQuotes.getFormattedPercentByInterval(
interval = stateToUpdate.selectedInterval,
),
priceChangeType = priceChangeType,
triggerPriceChange = trigger,
dateTimeText = MarketsDateTimeFormatters.getDefaultDateTimeString(
stateToUpdate.selectedInterval,
currentTimestamp = lastUpdatedTimestamp.value,
),
body = stateToUpdate.body.updatePricePerformance(newQuotes.currentPrice),
)
}
}
private fun MarketsTokenDetailsUM.Body.updatePricePerformance(price: BigDecimal): MarketsTokenDetailsUM.Body {
val currentPricePerformance = currentTokenInfo.value?.pricePerformance ?: return this
return if (this is MarketsTokenDetailsUM.Body.Content) {
copy(
infoBlocks = infoBlocks.copy(
pricePerformance = pricePerformanceConverter.convert(currentPricePerformance, price),
),
)
} else {
this
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.markets.details.impl.ui
import android.content.res.Configuration
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
@ -193,7 +194,7 @@ private fun TokenPriceText(
val fallColor = TangemTheme.colors.text.warning
val generalColor = TangemTheme.colors.text.primary1
val color = remember { Animatable(generalColor) }
val color = remember(generalColor) { Animatable(generalColor) }
EventEffect(triggerPriceChange) {
val nextColor = when (it) {
@ -268,12 +269,11 @@ fun PriceChangeInterval.getText(): TextReference {
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
Content(
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
addTopBarStatusBarInsets = false,
MarketsTokenDetailsContent(
state = MarketsTokenDetailsUM(
tokenName = "Token Name",
priceText = "$0.00000000324",
@ -303,6 +303,7 @@ private fun Preview() {
onBackClick = {},
backgroundColor = TangemTheme.colors.background.tertiary,
portfolioBlock = {},
addTopBarStatusBarPadding = false,
)
}
}

View file

@ -19,6 +19,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.markets.impl.R
import com.tangem.utils.StringsSigns
@Composable
internal fun Description(
@ -33,7 +34,7 @@ internal fun Description(
append(description.resolveReference())
}
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
append(" " + stringResource(R.string.common_read_more))
append(" " + stringResource(R.string.common_read_more).replace(' ', StringsSigns.NON_BREAKING_SPACE))
}
}

View file

@ -60,6 +60,7 @@ internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) {
title = stringResource(id = R.string.markets_token_details_blockchain_site),
links = state.blockchainSite,
onLinkClick = state.onLinkClick,
lastBlock = true,
)
}
},

View file

@ -124,15 +124,16 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
Text(
modifier = Modifier.weight(1f),
text = state.low,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
SpacerW8()
Text(
modifier = Modifier.weight(1f),
text = state.high,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,

View file

@ -126,9 +126,10 @@ private fun LazyListScope.loadingInfoBlocks() {
InsightsBlockPlaceholder(modifier = Modifier.blockPaddings())
}
item("securityScore-loading") {
SecurityScorePlaceHolder(modifier = Modifier.blockPaddings())
}
// TODO second markets iteration
// item("securityScore-loading") {
// SecurityScorePlaceHolder(modifier = Modifier.blockPaddings())
// }
item("metrics-loading") {
MetricsBlockPlaceholder(modifier = Modifier.blockPaddings())

View file

@ -1,7 +1,7 @@
package com.tangem.features.markets.details.impl.ui.state
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.ImmutableList
internal data class MetricsUM(
val metrics: PersistentList<InfoPointUM>,
val metrics: ImmutableList<InfoPointUM>,
)

View file

@ -30,7 +30,6 @@ internal fun EntryBottomSheetContent(
modifier: Modifier = Modifier,
) {
val primary = TangemTheme.colors.background.primary
val secondary = TangemTheme.colors.background.secondary
val backgroundColor = remember { Animatable(primary) }
LocalMainBottomSheetColor.current.value = backgroundColor.value
@ -73,7 +72,7 @@ private fun BackgroundColorEffects(
bottomSheetState: State<BottomSheetState>,
) {
val primary = TangemTheme.colors.background.primary
val secondary = TangemTheme.colors.background.secondary
val tertiary = TangemTheme.colors.background.tertiary
// Order of LaunchedEffects is important here
@ -81,7 +80,7 @@ private fun BackgroundColorEffects(
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.animateTo(
secondary,
tertiary,
animationSpec = tween(durationMillis = 500),
)
}
@ -99,7 +98,7 @@ private fun BackgroundColorEffects(
when (bottomSheetState.value) {
BottomSheetState.EXPANDED -> {
backgroundColor.animateTo(
secondary,
tertiary,
animationSpec = tween(durationMillis = 100),
)
}
@ -113,12 +112,12 @@ private fun BackgroundColorEffects(
}
}
LaunchedEffect(primary, secondary) {
LaunchedEffect(primary, tertiary) {
if (backgroundColor.isRunning) return@LaunchedEffect
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.snapTo(secondary)
backgroundColor.snapTo(tertiary)
}
MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.snapTo(primary)

View file

@ -17,6 +17,8 @@ import com.tangem.domain.markets.*
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM
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.flow.*
import kotlinx.coroutines.launch
@ -29,7 +31,7 @@ internal class TokenMarketBlockModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val getTokenQuotesUseCase: GetTokenQuotesUseCase,
private val getTokenQuotesUseCase: GetCurrencyQuotesUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
) : Model() {
@ -46,6 +48,7 @@ internal class TokenMarketBlockModel @Inject constructor(
)
private var quotesState: QuotesState? = null
private val quotesUpdateJobHolder = JobHolder()
val state = MutableStateFlow(
TokenMarketBlockUM(
@ -65,10 +68,11 @@ internal class TokenMarketBlockModel @Inject constructor(
private fun startFetching() {
modelScope.launch {
getTokenQuotesUseCase(
tokenId = params.tokenId,
currencyID = params.cryptoCurrencyID,
interval = PriceChangeInterval.H24,
refresh = true,
).collect {
it.onRight { res ->
it.onSome { res ->
quotesState = QuotesState(
currentPrice = res.fiatRate,
h24Percent = res.priceChange,
@ -90,7 +94,7 @@ internal class TokenMarketBlockModel @Inject constructor(
)
}
}
}
}.saveIn(quotesUpdateJobHolder)
modelScope.launch(dispatchers.main) {
val result = getTokenPriceChartUseCase(
@ -131,7 +135,6 @@ internal class TokenMarketBlockModel @Inject constructor(
),
)
// FIXME navigation crash [REDACTED_TASK_KEY]
router.push(
AppRoute.MarketsTokenDetails(
token = tokenParam,

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.charts.MarketChartMini
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.common.ui.tokens.TokenPriceText
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.TextShimmer
@ -83,19 +84,21 @@ private fun LeftSide(
FlowRow(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
Text(
text = priceText,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
TokenPriceText(
modifier = Modifier.alignByBaseline(),
price = priceText,
priceChangeType = type,
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
PriceChangeInPercent(
modifier = Modifier.alignByBaseline(),
valueInPercent = percentText,
type = type,
)
Text(
modifier = Modifier.alignByBaseline(),
text = stringResource(id = R.string.wallet_marketprice_block_update_time),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,

View file

@ -192,7 +192,7 @@ internal class MarketsListUMStateManager(
private fun state(): MarketsListUM = MarketsListUM(
list = ListUM.Loading,
searchBar = SearchBarUM(
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = { searchQuery = it },
isActive = false,

View file

@ -256,7 +256,7 @@ private fun Preview() {
onItemClick = {},
),
searchBar = SearchBarUM(
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = {},
isActive = false,

View file

@ -1,185 +1,46 @@
package com.tangem.features.markets.tokenlist.impl.ui.components
import android.content.res.Configuration
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.IntOffset
import com.tangem.common.ui.charts.MarketChartMini
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.common.ui.tokens.TokenPriceText
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.currency.icon.CoinIcon
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.windowsize.WindowSizeType
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import com.tangem.utils.StringsSigns.MINUS
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
import kotlin.random.Random
internal enum class DragValue { Start, End }
const val SWIPE_THRESHOLD_PERCENT = 0.8f
const val SWIPE_VELOCITY_THRESHOLD = 20f
@Suppress("LongMethod")
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MarketsListItem(
model: MarketsListItemUM,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
onSwipeToAction: () -> Unit = {},
) {
val actionWidth = TangemTheme.dimens.size68
val actionWidthPx = with(LocalDensity.current) { actionWidth.toPx() }
val hapticManager = LocalHapticManager.current
val anchors = DraggableAnchors {
DragValue.Start at 0f
DragValue.End at -actionWidthPx
}
val state = remember {
AnchoredDraggableState(
initialValue = DragValue.Start,
anchors = anchors,
positionalThreshold = { it * (1 - SWIPE_THRESHOLD_PERCENT) },
velocityThreshold = { SWIPE_VELOCITY_THRESHOLD },
animationSpec = tween(easing = FastOutSlowInEasing),
confirmValueChange = { it == DragValue.Start },
)
}
val dragInteractionSource = remember { MutableInteractionSource() }
val clickInteractionSource = remember { MutableInteractionSource() }
val isInDraggedState by dragInteractionSource.collectIsDraggedAsState()
LaunchedEffect(Unit) {
var actionPerformed = false
var releasePerformed = true
launch {
snapshotFlow { state.offset }
.collect {
val border = -actionWidthPx * SWIPE_THRESHOLD_PERCENT
if (it < border && actionPerformed.not()) {
hapticManager.perform(TangemHapticEffect.View.GestureThresholdActivate)
actionPerformed = true
releasePerformed = false
}
if (it > border) {
if (releasePerformed.not()) {
hapticManager.perform(TangemHapticEffect.View.GestureThresholdDeactivate)
releasePerformed = true
}
actionPerformed = false
}
}
}
launch {
snapshotFlow { isInDraggedState }
.collect {
if (it.not() && actionPerformed) {
releasePerformed = true
onSwipeToAction()
}
}
}
}
Box(
modifier = Modifier
.height(intrinsicSize = IntrinsicSize.Min)
.fillMaxWidth(),
) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.fillMaxHeight()
.offset {
IntOffset(
x = actionWidthPx.roundToInt() +
state
.requireOffset()
.toInt(),
y = 0,
)
}
.width(actionWidth)
.background(TangemTheme.colors.control.checked),
contentAlignment = Alignment.Center,
) {
Image(
modifier = Modifier.size(TangemTheme.dimens.size28),
imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_mini_28),
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.primary2),
contentDescription = null,
)
}
Box(
modifier = modifier
.align(Alignment.CenterStart)
.clip(RectangleShape)
.offset {
IntOffset(
x = state
.requireOffset()
.toInt(),
y = 0,
)
}
.anchoredDraggable(
state = state,
orientation = Orientation.Horizontal,
interactionSource = dragInteractionSource,
)
.clickable(
enabled = true,
interactionSource = clickInteractionSource,
indication = rememberRipple(),
onClick = onClick,
),
) {
MarketsListItemContent(model = model)
}
}
internal fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
MarketsListItemContent(
modifier = modifier
.fillMaxWidth()
.clip(RectangleShape)
.clickable(onClick = onClick),
model = model,
)
}
@Composable
@ -325,38 +186,6 @@ private fun RowScope.TokenMarketCapText(text: String) {
)
}
@Composable
private fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
val growColor = TangemTheme.colors.text.accent
val fallColor = TangemTheme.colors.text.warning
val generalColor = TangemTheme.colors.text.primary1
val color = remember { Animatable(generalColor) }
LaunchedEffect(price) {
if (priceChangeType != null) {
val nextColor = when (priceChangeType) {
PriceChangeType.UP,
-> growColor
PriceChangeType.DOWN -> fallColor
PriceChangeType.NEUTRAL -> return@LaunchedEffect
}
color.animateTo(nextColor, snap())
color.animateTo(generalColor, tween(durationMillis = 500))
}
}
Text(
modifier = modifier,
text = price,
color = color.value,
maxLines = 1,
style = TangemTheme.typography.body2,
overflow = TextOverflow.Visible,
)
}
@Composable
private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?) {
val chartWidth = TangemTheme.dimens.size56

View file

@ -88,6 +88,7 @@ internal class TokenDetailsFragment : ComposeFragment() {
val tokenId = id.rawCurrencyId ?: return null // token price is not available
return TokenMarketBlockComponent.Params(
cryptoCurrencyID = id,
tokenId = tokenId,
tokenName = name,
tokenSymbol = symbol,