Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-08 18:53:25 +03:00
parent 700731a408
commit c500f45307
59 changed files with 1338 additions and 278 deletions

View file

@ -14,6 +14,7 @@ android {
dependencies {
/* Project - API */
api(projects.features.markets.api)
implementation(projects.core.navigation)
/* Domain */
implementation(projects.domain.markets)

View file

@ -5,6 +5,9 @@ import arrow.core.getOrElse
import com.tangem.common.ui.charts.state.*
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
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.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
@ -13,11 +16,16 @@ 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.features.markets.details.api.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM
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.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.impl.R
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
@ -28,12 +36,15 @@ import java.math.BigDecimal
import java.math.RoundingMode
import javax.inject.Inject
@Suppress("LargeClass")
@Stable
internal class MarketsTokenDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
private val urlOpener: UrlOpener,
) : Model() {
val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
@ -47,6 +58,21 @@ internal class MarketsTokenDetailsModel @Inject constructor(
initialValue = params.appCurrency,
)
private val infoConverter = TokenMarketInfoConverter(
appCurrency = Provider { currentAppCurrency.value },
onInfoClick = {
showInfoBottomSheet(it)
},
onLinkClick = {
urlOpener.openUrl(it.url)
},
)
private val descriptionConverter = DescriptionConverter(
onReadModeClicked = {
showInfoBottomSheet(it)
},
)
private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) {
chartData = MarketChartData.NoData.Loading
@ -89,19 +115,39 @@ internal class MarketsTokenDetailsModel @Inject constructor(
chartState = MarketsTokenDetailsUM.ChartState(
dataProducer = chartDataProducer,
chartLook = MarketChartLook(),
onLoadRetryClick = {},
onLoadRetryClick = ::onLoadRetryClicked,
status = MarketsTokenDetailsUM.ChartState.Status.LOADING,
onMarkerPointSelected = ::onMarkerPointSelected,
),
selectedInterval = PriceChangeInterval.H24,
onSelectedIntervalChange = ::onSelectedIntervalChange,
body = MarketsTokenDetailsUM.Body.Loading,
infoBottomSheet = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
),
)
private val loadChartJobHolder = JobHolder()
init {
loadChart(PriceChangeInterval.H24)
// reload screen if currency changed
modelScope.launch {
currentAppCurrency
.filter { it != params.appCurrency }
.collectLatest {
initialLoad()
}
}
initialLoad()
}
private fun initialLoad() {
loadChart(state.value.selectedInterval)
loadInfo()
}
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
@ -175,12 +221,60 @@ internal class MarketsTokenDetailsModel @Inject constructor(
chartState = it.chartState.copy(
status = MarketsTokenDetailsUM.ChartState.Status.ERROR,
),
body = if (it.body is MarketsTokenDetailsUM.Body.Error) {
MarketsTokenDetailsUM.Body.Nothing
} else {
it.body
},
)
}
}
}.saveIn(loadChartJobHolder)
}
private fun loadInfo() {
state.update {
it.copy(
body = MarketsTokenDetailsUM.Body.Loading,
)
}
modelScope.launch {
val tokenMarketInfo = getTokenMarketInfoUseCase(
appCurrency = currentAppCurrency.value,
tokenId = params.token.id,
)
tokenMarketInfo.fold(
ifRight = { result ->
state.update {
it.copy(
body = MarketsTokenDetailsUM.Body.Content(
description = descriptionConverter.convert(result),
infoBlocks = infoConverter.convert(result),
),
)
}
},
ifLeft = {
state.update {
if (it.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) {
it.copy(
body = MarketsTokenDetailsUM.Body.Error(
onLoadRetryClick = ::onLoadRetryClicked,
),
)
} else {
it.copy(
body = MarketsTokenDetailsUM.Body.Nothing,
)
}
}
},
)
}
}
private fun getFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String {
return when (interval) {
PriceChangeInterval.H24 -> { value: BigDecimal ->
@ -251,4 +345,40 @@ internal class MarketsTokenDetailsModel @Inject constructor(
MarketChartLook.Type.Falling
}
}
private fun showInfoBottomSheet(content: InfoBottomSheetContent) {
state.update { stateToUpdate ->
stateToUpdate.copy(
infoBottomSheet = stateToUpdate.infoBottomSheet.copy(
isShow = true,
onDismissRequest = ::hideInfoBottomSheet,
content = content,
),
)
}
}
private fun hideInfoBottomSheet() {
state.update { stateToUpdate ->
stateToUpdate.copy(
infoBottomSheet = stateToUpdate.infoBottomSheet.copy(
isShow = false,
),
)
}
}
private fun onLoadRetryClicked() {
val currentState = state.value
if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.ERROR) {
loadChart(currentState.selectedInterval)
}
if (currentState.body is MarketsTokenDetailsUM.Body.Error ||
currentState.body is MarketsTokenDetailsUM.Body.Nothing
) {
loadInfo()
}
}
}

View file

@ -0,0 +1,41 @@
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.domain.markets.TokenMarketInfo
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
import com.tangem.utils.converter.Converter
@Stable
internal class DescriptionConverter(
val onReadModeClicked: (InfoBottomSheetContent) -> Unit,
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.Description?> {
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.Description? {
return value.shortDescription?.let { desc ->
MarketsTokenDetailsUM.Description(
shortDescription = stringReference(desc),
fullDescription = value.fullDescription?.let { fullDescription ->
stringReference(fullDescription)
},
onReadMoreClick = {
onReadModeClicked(
InfoBottomSheetContent(
title = resourceReference(
R.string.markets_token_details_about_token_title,
wrappedList(
value.name,
),
),
body = stringReference(value.fullDescription ?: ""),
),
)
},
)
}
}
}

View file

@ -0,0 +1,131 @@
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.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
import com.tangem.features.markets.details.impl.ui.state.InsightsUM
import com.tangem.features.markets.impl.R
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 java.math.BigDecimal
@Stable
internal class InsightsConverter(
private val appCurrency: Provider<AppCurrency>,
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
) : Converter<TokenMarketInfo.Insights, InsightsUM> {
override fun convert(value: TokenMarketInfo.Insights): InsightsUM {
return with(value) {
InsightsUM(
h24Info = createInfoPointList(
experiencedBuyerChange = experiencedBuyerChange?.day,
holdersChange = holdersChange?.day,
liquidityChange = liquidityChange?.day,
buyPressureChange = buyPressureChange?.day,
),
weekInfo = createInfoPointList(
experiencedBuyerChange = experiencedBuyerChange?.week,
holdersChange = holdersChange?.week,
liquidityChange = liquidityChange?.week,
buyPressureChange = buyPressureChange?.week,
),
monthInfo = createInfoPointList(
experiencedBuyerChange = experiencedBuyerChange?.month,
holdersChange = holdersChange?.month,
liquidityChange = liquidityChange?.month,
buyPressureChange = buyPressureChange?.month,
),
)
}
}
private fun createInfoPointList(
experiencedBuyerChange: BigDecimal?,
holdersChange: BigDecimal?,
liquidityChange: BigDecimal?,
buyPressureChange: BigDecimal?,
): ImmutableList<InfoPointUM> {
return persistentListOf(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
value = experiencedBuyerChange.convertChange(),
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),
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(),
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(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_liquidity),
body = resourceReference(R.string.markets_token_details_liquidity_description),
),
)
},
),
)
}
private fun BigDecimal?.convertChange(isFiatValue: Boolean = false): String {
if (this == null) return StringsSigns.DASH_SIGN
val value = if (isFiatValue) {
val currency = appCurrency()
BigDecimalFormatter.formatCompactFiatAmount(
amount = this.abs(),
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
)
} else {
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 -> value
else -> StringsSigns.DASH_SIGN
}
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.LinksUM
import com.tangem.features.markets.impl.R
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
@Stable
internal class LinksConverter(
private val onLinkClick: (LinksUM.Link) -> Unit,
) : Converter<TokenMarketInfo.Links, LinksUM> {
override fun convert(value: TokenMarketInfo.Links): LinksUM {
return LinksUM(
officialLinks = value.officialLinks?.map { it.convert() }.orEmpty().toImmutableList(),
social = value.social?.map { it.convert() }.orEmpty().toImmutableList(),
repository = value.repository?.map { it.convert() }.orEmpty().toImmutableList(),
blockchainSite = value.blockchainSite?.map { it.convert() }.orEmpty().toImmutableList(),
onLinkClick = onLinkClick,
)
}
private fun TokenMarketInfo.Link.convert(): LinksUM.Link {
return LinksUM.Link(
title = stringReference(title),
iconRes = getIconById(id),
url = link,
)
}
private fun getIconById(id: String?): Int {
return when (id) {
"linkedin" -> R.drawable.ic_linkedin_24
"discord" -> R.drawable.ic_discord_24
"youtube" -> R.drawable.ic_youtube_24
"telegram" -> R.drawable.ic_telegram_24
"github" -> R.drawable.ic_github_24
"twitter" -> R.drawable.ic_twitter_24
"facebook" -> R.drawable.ic_facebook_24
"reddit" -> R.drawable.ic_reddit_24
"instagram" -> R.drawable.ic_instagram_24
"whitepaper" -> R.drawable.ic_doc_24
else -> R.drawable.ic_arrow_top_right_24
}
}
}

View file

@ -0,0 +1,135 @@
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.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
import com.tangem.features.markets.details.impl.ui.state.MetricsUM
import com.tangem.features.markets.impl.R
import com.tangem.utils.Provider
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(
private val appCurrency: Provider<AppCurrency>,
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
) : Converter<TokenMarketInfo.Metrics, MetricsUM> {
@Suppress("LongMethod")
override fun convert(value: TokenMarketInfo.Metrics): MetricsUM {
return with(value) {
MetricsUM(
metrics = persistentListOf(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_market_capitalization),
value = marketCap.formatAmount(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_market_capitalization),
body = resourceReference(
R.string.markets_token_details_market_capitalization_description,
),
),
)
},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_market_rating),
value = marketRating?.toString() ?: StringsSigns.DASH_SIGN,
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_market_rating),
body = resourceReference(R.string.markets_token_details_market_rating_description),
),
)
},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_trading_volume),
value = volume24h.formatAmount(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_trading_volume),
body = resourceReference(
R.string.markets_token_details_trading_volume_24h_description,
),
),
)
},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
value = fullyDilutedValuation.formatAmount(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_fully_diluted_valuation),
body = resourceReference(
R.string.markets_token_details_fully_diluted_valuation_description,
),
),
)
},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_circulating_supply),
value = circulatingSupply.formatAmount(crypto = true),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_circulating_supply),
body = resourceReference(
R.string.markets_token_details_circulating_supply_description,
),
),
)
},
),
InfoPointUM(
title = resourceReference(R.string.markets_token_details_total_supply),
value = totalSupply.formatAmount(crypto = true),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_total_supply),
body = resourceReference(R.string.markets_token_details_total_supply_description),
),
)
},
),
),
)
}
}
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)
} else {
val currency = appCurrency()
BigDecimalFormatter.formatFiatAmount(
fiatAmount = this,
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
decimals = 0,
)
}
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
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 {
return PricePerformanceUM(
h24 = value.day.convert(),
month = value.month.convert(),
all = value.allTime.convert(),
)
}
private fun TokenMarketInfo.Range?.convert(): PricePerformanceUM.Value {
if (this == null) {
return PricePerformanceUM.Value(
low = StringsSigns.DASH_SIGN,
high = StringsSigns.DASH_SIGN,
indicatorFraction = 0f,
)
}
return PricePerformanceUM.Value(
low = low.convert(),
high = high.convert(),
indicatorFraction = calculateFraction(),
)
}
private fun BigDecimal?.convert(): String {
val currency = appCurrency()
return BigDecimalFormatter.formatCompactFiatAmount(
amount = this,
fiatCurrencyCode = currency.code,
fiatCurrencySymbol = currency.symbol,
)
}
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)
}
}

View file

@ -0,0 +1,35 @@
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.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM
import com.tangem.features.markets.impl.R
import com.tangem.utils.converter.Converter
// TODO implement when backend is ready
@Stable
internal class SecurityScoreConverter(
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
) : Converter<Unit, SecurityScoreUM> {
override fun convert(value: Unit): SecurityScoreUM {
return with(value) {
SecurityScoreUM(
score = 4.7f,
description = "Based on 3 ratings",
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_security_score),
body = stringReference("markets_token_details_security_score_description"),
// FIXME
// resourceReference(R.string.markets_token_details_security_score_description)
),
)
},
)
}
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.LinksUM
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
@Stable
internal class TokenMarketInfoConverter(
appCurrency: Provider<AppCurrency>,
onInfoClick: (InfoBottomSheetContent) -> Unit,
onLinkClick: (LinksUM.Link) -> Unit,
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.InformationBlocks> {
private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick)
private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
private val pricePerformanceConverter = PricePerformanceConverter(appCurrency = appCurrency)
private val linksConverter = LinksConverter(onLinkClick = onLinkClick)
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks {
return MarketsTokenDetailsUM.InformationBlocks(
insights = value.insights?.let { insightsConverter.convert(it) },
securityScore = securityScoreConverter.convert(Unit),
metrics = value.metrics?.let { metricsConverter.convert(it) },
pricePerformance = value.pricePerformance?.let { pricePerformanceConverter.convert(it) },
links = value.links?.let { linksConverter.convert(it) },
)
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.features.markets.details.impl.ui
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.ui.Alignment
@ -12,12 +14,11 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.common.ui.charts.state.MarketChartDataProducer
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
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
@ -29,9 +30,12 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.disableNestedScroll
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.markets.details.impl.ui.components.InfoBottomSheet
import com.tangem.features.markets.details.impl.ui.components.MarketTokenDetailsChart
import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM
import com.tangem.features.markets.details.impl.ui.components.tokenMarketDetailsBody
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.persistentListOf
@ -44,11 +48,13 @@ internal fun MarketsTokenDetailsContent(
modifier: Modifier = Modifier,
) {
Content(
modifier = modifier,
state = state,
onBackClick = onBackClick,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)
InfoBottomSheet(config = state.infoBottomSheet)
}
@Suppress("UnusedPrivateMember")
@ -61,6 +67,7 @@ private fun Content(
) {
val backgroundColor = LocalMainBottomSheetColor.current.value
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
Column(
modifier = modifier
@ -78,31 +85,46 @@ private fun Content(
title = state.tokenName,
startButton = TopAppBarButtonUM.Back(onBackClick),
)
SpacerH4()
Header(
state = state,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
LazyColumn(
modifier = Modifier.disableNestedScroll(),
contentPadding = PaddingValues(bottom = bottomBarHeight),
) {
item("header") {
Header(
state = state,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
}
item { SpacerH16() }
item("intervalSelector") {
IntervalSelector(
trendInterval = state.selectedInterval,
onIntervalClick = state.onSelectedIntervalChange,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
}
item { SpacerH32() }
item(
contentType = "chart",
) {
MarketTokenDetailsChart(
modifier = Modifier.fillMaxWidth(),
state = state.chartState,
)
}
item { SpacerH16() }
SpacerH16()
IntervalSelector(
trendInterval = state.selectedInterval,
onIntervalClick = state.onSelectedIntervalChange,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
SpacerH32()
MarketTokenDetailsChart(
modifier = Modifier.fillMaxWidth(),
state = state.chartState,
)
tokenMarketDetailsBody(
state = state.body,
)
}
}
}
@ -199,6 +221,7 @@ fun PriceChangeInterval.getText(): TextReference {
private fun Preview() {
TangemThemePreview {
Content(
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
state = MarketsTokenDetailsUM(
tokenName = "Token Name",
priceText = "Price",
@ -215,6 +238,12 @@ private fun Preview() {
),
selectedInterval = PriceChangeInterval.H24,
onSelectedIntervalChange = { },
body = MarketsTokenDetailsUM.Body.Loading,
infoBottomSheet = TangemBottomSheetConfig(
isShow = false,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
),
onHeaderSizeChange = {},
onBackClick = {},

View file

@ -0,0 +1,112 @@
package com.tangem.features.markets.details.impl.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.markets.impl.R
@Composable
internal fun Description(
description: TextReference,
hasFullDescription: Boolean,
onReadMoreClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (hasFullDescription) {
val text = buildAnnotatedString {
withStyle(SpanStyle(color = TangemTheme.colors.text.secondary)) {
append(description.resolveReference())
}
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
append(" " + stringResource(R.string.common_read_more))
}
}
ClickableText(
modifier = modifier,
text = text,
style = TangemTheme.typography.body2,
) {
text.spanStyles.getOrNull(1)?.let { spanStyle ->
if (it in spanStyle.start..spanStyle.end) {
onReadMoreClick()
}
}
}
} else {
Text(
modifier = modifier,
text = description.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
@Composable
internal fun DescriptionPlaceholder(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
) {
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
TextShimmer(
modifier = Modifier.fillMaxWidth(fraction = 0.8f),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
}
}
@Preview
@Composable
private fun ContentPreview() {
TangemThemePreview {
Description(
description = stringReference(
"XRP (XRP) is a cryptocurrency launched in January 2009, where the first " +
"genesis block was mined on 9th January 2009",
),
hasFullDescription = true,
onReadMoreClick = {},
)
}
}
@Preview
@Composable
private fun PreviewPlaceholder() {
TangemThemePreview {
PreviewShimmerContainer(
actualContent = {
ContentPreview()
},
shimmerContent = {
DescriptionPlaceholder()
},
)
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.features.markets.details.impl.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
@Composable
internal fun InfoBottomSheet(config: TangemBottomSheetConfig) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
TangemBottomSheet<InfoBottomSheetContent>(
config = config,
skipPartiallyExpanded = false,
addBottomInsets = false,
title = {
TangemBottomSheetTitle(title = it.title)
},
content = {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = TangemTheme.dimens.spacing28),
) {
Text(
text = it.body.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
SpacerH(bottomBarHeight)
}
},
)
}

View file

@ -1,7 +1,6 @@
package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
@ -15,11 +14,10 @@ import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.text.TooltipText
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemAnimations
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.details.impl.ui.entity.InfoPointUM
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
@Composable
internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) {
@ -42,18 +40,11 @@ internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier)
overflow = TextOverflow.Ellipsis,
)
}
AnimatedContent(
modifier = Modifier,
targetState = infoPointUM.value,
transitionSpec = { TangemAnimations.transitionSpecs.textChange },
label = "insight block",
) {
Text(
text = it,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
Text(
text = infoPointUM.value,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
}

View file

@ -20,8 +20,8 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.features.markets.details.impl.ui.entity.InfoPointUM
import com.tangem.features.markets.details.impl.ui.entity.InsightsUM
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
import com.tangem.features.markets.details.impl.ui.state.InsightsUM
import com.tangem.features.markets.details.impl.ui.getText
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.persistentListOf

View file

@ -9,7 +9,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.charts.downsample.fastForEach
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.SmallButtonShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
@ -21,7 +21,7 @@ import com.tangem.core.ui.extensions.stringReference
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.details.impl.ui.entity.LinksUM
import com.tangem.features.markets.details.impl.ui.state.LinksUM
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -69,8 +69,8 @@ internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) {
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun SubBlock(
links: ImmutableList<LinksUM.LinkUM>,
onLinkClick: (LinksUM.LinkUM) -> Unit,
links: ImmutableList<LinksUM.Link>,
onLinkClick: (LinksUM.Link) -> Unit,
modifier: Modifier = Modifier,
lastBlock: Boolean = false,
title: String = "Official links",
@ -166,36 +166,36 @@ private fun ContentPreview() {
LinksBlock(
state = LinksUM(
officialLinks = persistentListOf(
LinksUM.LinkUM(
LinksUM.Link(
title = stringReference("Website"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.LinkUM(
LinksUM.Link(
title = stringReference("Website"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.LinkUM(
LinksUM.Link(
title = stringReference("Website"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
),
social = persistentListOf(
LinksUM.LinkUM(
LinksUM.Link(
title = stringReference("Twitter"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.LinkUM(
LinksUM.Link(
title = stringReference("Facebook"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
),
repository = persistentListOf(
LinksUM.LinkUM(
LinksUM.Link(
title = stringReference("Github"),
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",

View file

@ -10,11 +10,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import com.tangem.common.ui.charts.MarketChart
import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.common.ui.charts.state.rememberMarketChartState
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.entity.MarketsTokenDetailsUM
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
@Composable
@ -34,6 +35,7 @@ internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, mo
)
val backgroundColor = LocalMainBottomSheetColor.current.value
val bottomChartAxisHeight = getMarketChartBottomAxisHeight()
Box(modifier) {
MarketChart(
@ -45,7 +47,8 @@ internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, mo
Box(
Modifier
.drawBehind { drawRect(backgroundColor) }
.matchParentSize(),
.matchParentSize()
.padding(bottom = bottomChartAxisHeight),
) {
when (state.status) {
MarketsTokenDetailsUM.ChartState.Status.LOADING -> {

View file

@ -17,8 +17,8 @@ import com.tangem.core.ui.extensions.resourceReference
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.details.impl.ui.entity.InfoPointUM
import com.tangem.features.markets.details.impl.ui.entity.MetricsUM
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
import com.tangem.features.markets.details.impl.ui.state.MetricsUM
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList

View file

@ -26,7 +26,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.domain.markets.PriceChangeInterval
import com.tangem.features.markets.details.impl.ui.entity.PricePerformanceUM
import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM
import com.tangem.features.markets.details.impl.ui.getText
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.persistentListOf
@ -127,30 +127,18 @@ private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifi
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
AnimatedContent(
targetState = state.low,
transitionSpec = { TangemAnimations.transitionSpecs.textChange },
label = "Low price",
) {
Text(
text = it,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
Text(
text = state.low,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
SpacerW8()
AnimatedContent(
targetState = state.high,
transitionSpec = { TangemAnimations.transitionSpecs.textChange },
label = "High price",
) {
Text(
text = it,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
)
}
Text(
text = state.high,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
)
}
}
}

View file

@ -27,7 +27,7 @@ import com.tangem.core.ui.extensions.resourceReference
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.details.impl.ui.entity.SecurityScoreUM
import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM
import com.tangem.features.markets.impl.R
import kotlin.math.round

View file

@ -0,0 +1,142 @@
package com.tangem.features.markets.details.impl.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
internal fun LazyListScope.tokenMarketDetailsBody(state: MarketsTokenDetailsUM.Body) {
when (state) {
MarketsTokenDetailsUM.Body.Loading -> {
loading()
}
is MarketsTokenDetailsUM.Body.Content -> {
if (state.description != null) {
description(state.description)
}
infoBlocksList(state.infoBlocks)
}
is MarketsTokenDetailsUM.Body.Error -> {
error(state)
}
MarketsTokenDetailsUM.Body.Nothing -> {
// Do nothing
}
}
}
private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) {
item("body-error") {
Box(Modifier.fillMaxWidth()) {
UnableToLoadData(
modifier = Modifier
.align(Alignment.Center)
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing40,
),
onRetryClick = state.onLoadRetryClick,
)
}
}
}
private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) {
item("description") {
Description(
modifier = Modifier.blockPaddings(),
description = description.shortDescription,
hasFullDescription = description.fullDescription != null,
onReadMoreClick = description.onReadMoreClick,
)
}
}
internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) {
if (state.insights != null) {
item("insights") {
InsightsBlock(
modifier = Modifier.blockPaddings(),
state = state.insights,
)
}
}
if (state.securityScore != null) {
item("securityScore") {
SecurityScoreBlock(
modifier = Modifier.blockPaddings(),
state = state.securityScore,
)
}
}
if (state.metrics != null) {
item("metrics") {
MetricsBlock(
modifier = Modifier.blockPaddings(),
state = state.metrics,
)
}
}
if (state.pricePerformance != null) {
item("pricePerformance") {
PricePerformanceBlock(
modifier = Modifier.blockPaddings(),
state = state.pricePerformance,
)
}
}
if (state.links != null) {
item("links") {
LinksBlock(
modifier = Modifier.blockPaddings(),
state = state.links,
)
}
}
}
private fun LazyListScope.loading() {
item("description-loading") {
DescriptionPlaceholder(modifier = Modifier.blockPaddings())
}
item("insights-loading") {
InsightsBlockPlaceholder(modifier = Modifier.blockPaddings())
}
item("securityScore-loading") {
SecurityScorePlaceHolder(modifier = Modifier.blockPaddings())
}
item("metrics-loading") {
MetricsBlockPlaceholder(modifier = Modifier.blockPaddings())
}
item("pricePerformance-loading") {
PricePerformanceBlockPlaceholder(modifier = Modifier.blockPaddings())
}
item("links-loading") {
LinksBlockPlaceholder(modifier = Modifier.blockPaddings())
}
}
@Composable
private fun Modifier.blockPaddings(): Modifier {
return this.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing12,
)
}

View file

@ -1,11 +0,0 @@
package com.tangem.features.markets.details.impl.ui.entity
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.PersistentList
@Immutable
internal data class InsightsUM(
val h24Info: PersistentList<InfoPointUM>,
val weekInfo: PersistentList<InfoPointUM>,
val monthInfo: PersistentList<InfoPointUM>,
)

View file

@ -1,22 +0,0 @@
package com.tangem.features.markets.details.impl.ui.entity
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.PersistentList
@Immutable
internal data class LinksUM(
val officialLinks: PersistentList<LinkUM>,
val social: PersistentList<LinkUM>,
val repository: PersistentList<LinkUM>,
val blockchainSite: PersistentList<LinkUM>,
val onLinkClick: (LinkUM) -> Unit,
) {
@Immutable
data class LinkUM(
@DrawableRes val iconRes: Int,
val title: TextReference,
val url: String,
)
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.markets.details.impl.ui.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
internal data class InfoBottomSheetContent(
val title: TextReference,
val body: TextReference,
) : TangemBottomSheetConfigContent

View file

@ -1,9 +1,7 @@
package com.tangem.features.markets.details.impl.ui.entity
package com.tangem.features.markets.details.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal data class InfoPointUM(
val title: TextReference,
val value: String,

View file

@ -0,0 +1,9 @@
package com.tangem.features.markets.details.impl.ui.state
import kotlinx.collections.immutable.ImmutableList
internal data class InsightsUM(
val h24Info: ImmutableList<InfoPointUM>,
val weekInfo: ImmutableList<InfoPointUM>,
val monthInfo: ImmutableList<InfoPointUM>,
)

View file

@ -0,0 +1,19 @@
package com.tangem.features.markets.details.impl.ui.state
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
internal data class LinksUM(
val officialLinks: ImmutableList<Link>,
val social: ImmutableList<Link>,
val repository: ImmutableList<Link>,
val blockchainSite: ImmutableList<Link>,
val onLinkClick: (Link) -> Unit,
) {
data class Link(
@DrawableRes val iconRes: Int,
val title: TextReference,
val url: String,
)
}

View file

@ -1,14 +1,14 @@
package com.tangem.features.markets.details.impl.ui.entity
package com.tangem.features.markets.details.impl.ui.state
import androidx.compose.runtime.Immutable
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.extensions.TextReference
import com.tangem.domain.markets.PriceChangeInterval
import java.math.BigDecimal
@Immutable
internal data class MarketsTokenDetailsUM(
val tokenName: String,
val priceText: String,
@ -19,10 +19,10 @@ internal data class MarketsTokenDetailsUM(
val selectedInterval: PriceChangeInterval,
val chartState: ChartState,
val onSelectedIntervalChange: (PriceChangeInterval) -> Unit,
// val info : Information TODO [REDACTED_TASK_KEY]
val infoBottomSheet: TangemBottomSheetConfig,
val body: Body,
) {
@Immutable
data class ChartState(
val status: Status,
val dataProducer: MarketChartDataProducer,
@ -30,18 +30,39 @@ internal data class MarketsTokenDetailsUM(
val onLoadRetryClick: () -> Unit,
val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit,
) {
@Immutable
enum class Status {
LOADING, ERROR, DATA
}
}
@Immutable
data class Information(
data class InformationBlocks(
val insights: InsightsUM?,
val securityScore: SecurityScoreUM?,
val metrics: MetricsUM?,
val pricePerformance: PricePerformanceUM?,
val links: LinksUM?,
)
@Immutable
sealed interface Body {
data class Error(
val onLoadRetryClick: () -> Unit,
) : Body
data object Loading : Body
data class Content(
val description: Description?,
val infoBlocks: InformationBlocks,
) : Body
data object Nothing : Body
}
data class Description(
val shortDescription: TextReference,
val fullDescription: TextReference?,
val onReadMoreClick: () -> Unit,
)
}

View file

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

View file

@ -1,15 +1,12 @@
package com.tangem.features.markets.details.impl.ui.entity
package com.tangem.features.markets.details.impl.ui.state
import androidx.annotation.FloatRange
import androidx.compose.runtime.Immutable
@Immutable
internal data class PricePerformanceUM(
val h24: Value,
val month: Value,
val all: Value,
) {
@Immutable
data class Value(
val low: String,
val high: String,

View file

@ -1,9 +1,7 @@
package com.tangem.features.markets.details.impl.ui.entity
package com.tangem.features.markets.details.impl.ui.state
import androidx.annotation.FloatRange
import androidx.compose.runtime.Immutable
@Immutable
internal data class SecurityScoreUM(
@FloatRange(from = 0.0, to = 5.0) val score: Float,
val description: String,

View file

@ -11,9 +11,9 @@ import com.tangem.domain.markets.TokenMarket
import com.tangem.features.markets.component.BottomSheetState
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager
import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
@ -200,10 +200,12 @@ internal class MarketsListModel @Inject constructor(
modelScope.launch {
marketsListUMStateManager.searchQueryFlow
.filter { it.isNotEmpty() }
.debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS)
.distinctUntilChanged()
.filter { activeListManager == searchMarketsListManager }
.onEach {
if (it.isEmpty()) searchMarketsListManager.clearStateAndStopAllActions()
}
.filter { it.isNotEmpty() && activeListManager == searchMarketsListManager }
.collectLatest {
searchMarketsListManager.reload(searchText = it)
}
@ -229,6 +231,7 @@ internal class MarketsListModel @Inject constructor(
}
}
}
private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) {
launch {
while (true) {

View file

@ -7,8 +7,8 @@ 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.TokenMarket
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM.TrendInterval
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
@ -70,10 +70,11 @@ internal class MarketsTokenItemConverter(
private fun TokenMarket.getMarketCap(): String? {
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
return BigDecimalFormatter.formatCompactAmount(
value,
return BigDecimalFormatter.formatCompactFiatAmount(
amount = value,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
threeDigitsMethod = true,
)
}

View file

@ -6,9 +6,9 @@ import com.tangem.features.markets.tokenlist.impl.model.converters.MarketsTokenI
import com.tangem.features.markets.tokenlist.impl.model.utils.logAction
import com.tangem.features.markets.tokenlist.impl.model.utils.logStatus
import com.tangem.features.markets.tokenlist.impl.model.utils.logUpdateResults
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM.TrendInterval
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM.TrendInterval
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchFetchResult

View file

@ -7,11 +7,11 @@ import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList

View file

@ -38,10 +38,10 @@ import com.tangem.features.markets.component.BottomSheetState
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet
import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList

View file

@ -46,7 +46,7 @@ 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.entity.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider
import com.tangem.utils.StringsSigns.MINUS
import kotlinx.coroutines.launch

View file

@ -9,10 +9,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.buttons.SecondarySmallButton
@ -20,8 +16,9 @@ import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.disableNestedScroll
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.entity.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import kotlinx.coroutines.launch
private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50
@ -54,7 +51,7 @@ internal fun MarketsListLazyColumn(
if (state is ListUM.Loading) {
LazyColumn(
modifier = Modifier.nestedScroll(DisableParentConnection),
modifier = Modifier.disableNestedScroll(),
state = rememberLazyListState(),
contentPadding = PaddingValues(bottom = bottomBarHeight),
userScrollEnabled = false,
@ -65,7 +62,7 @@ internal fun MarketsListLazyColumn(
}
} else {
LazyColumn(
modifier = modifier.nestedScroll(DisableParentConnection),
modifier = modifier.disableNestedScroll(),
state = lazyListState,
contentPadding = PaddingValues(bottom = bottomBarHeight),
userScrollEnabled = true,
@ -221,10 +218,4 @@ fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buf
emitted = onLoadMore()
}
}
}
private object DisableParentConnection : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return available.copy(x = 0f)
}
}

View file

@ -19,8 +19,8 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByBottomSheetContentUM
import com.tangem.features.markets.tokenlist.impl.ui.entity.SortByTypeUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
@Composable
fun MarketsListSortByBottomSheet(config: TangemBottomSheetConfig) {

View file

@ -4,7 +4,7 @@ package com.tangem.features.markets.tokenlist.impl.ui.preview
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.features.markets.tokenlist.impl.ui.entity.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import kotlinx.collections.immutable.persistentListOf
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(

View file

@ -1,4 +1,4 @@
package com.tangem.features.markets.tokenlist.impl.ui.entity
package com.tangem.features.markets.tokenlist.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.charts.state.MarketChartLook

View file

@ -1,4 +1,4 @@
package com.tangem.features.markets.tokenlist.impl.ui.entity
package com.tangem.features.markets.tokenlist.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig

View file

@ -1,4 +1,4 @@
package com.tangem.features.markets.tokenlist.impl.ui.entity
package com.tangem.features.markets.tokenlist.impl.ui.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent