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

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
import com.tangem.domain.markets.GetTokenPriceChartUseCase
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import dagger.Module
@ -26,4 +27,10 @@ object MarketsDomainModule {
fun provideGetTokenPriceChartUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenPriceChartUseCase {
return GetTokenPriceChartUseCase(marketsTokenRepository = marketsTokenRepository)
}
@Provides
@Singleton
fun provideGetTokenMarketInfoUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenMarketInfoUseCase {
return GetTokenMarketInfoUseCase(marketsTokenRepository = marketsTokenRepository)
}
}

View file

@ -10,6 +10,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
@ -20,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.resolveAsTypeface
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent
@ -71,7 +73,6 @@ private const val GUIDELINES_COUNT = 3
* @param splitChartSegmentColor The color of the grayed by marker chart segment.
* @param backgroundSplitChartSegmentColorAlpha The alpha of the background the [splitChartSegmentColor]
* @param backgroundColorAlpha The alpha of the background color of the chart.
* @param noChartContent A composable function that defines the content to be displayed when there is no data to display.
*/
@Composable
fun MarketChart(
@ -108,7 +109,7 @@ fun MarketChart(
// we need to calculate what the overall height should be in order to get the correct height of the graph
val bottomAxisHeight = with(LocalDensity.current) {
TangemTheme.typography.caption2.fontSize.toPx().toInt() + TangemTheme.dimens.spacing26.toPx().toInt()
getMarketChartBottomAxisHeight().toPx().toInt()
}
CartesianChartHost(
@ -120,6 +121,10 @@ fun MarketChart(
} else {
0
}
}
// Sometimes the chart is not drawn correctly (ex. in LazyLayout), so we need to force the redraw
.drawBehind {
state.markerFraction
},
chart = chart,
modelProducer = state.modelProducer,
@ -129,6 +134,13 @@ fun MarketChart(
)
}
@Composable
fun getMarketChartBottomAxisHeight(): Dp {
return with(LocalDensity.current) {
TangemTheme.typography.caption2.fontSize.toDp() + TangemTheme.dimens.spacing26
}
}
@Composable
private fun rememberMarketVisibilityListener(
canvasWidth: Int,

View file

@ -238,7 +238,7 @@ private data class Area(
}
}
inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
private inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
for (index in indices) {
val item = get(index)
action(item)

View file

@ -24,6 +24,7 @@ interface TangemTechMarketsApi {
suspend fun getCoinMarketData(
@Path("coin_id") coinId: String,
@Query("currency") currency: String,
@Query("language") language: String,
): ApiResponse<TokenMarketInfoResponse>
@GET("coins/{coin_id}/history")

View file

@ -21,7 +21,7 @@ data class TokenMarketInfoResponse(
@Json(name = "full_description")
val fullDescription: String?,
@Json(name = "insights")
val insights: List<Insight>?,
val insights: Insights?,
@Json(name = "metrics")
val metrics: Metrics?,
@Json(name = "links")
@ -32,33 +32,33 @@ data class TokenMarketInfoResponse(
data class PriceChangePercentage(
@Json(name = "24h")
val day: Int?,
val day: BigDecimal?,
@Json(name = "1w")
val week: Int?,
val week: BigDecimal?,
@Json(name = "1m")
val month: Int?,
val month: BigDecimal?,
@Json(name = "3m")
val threeMonths: Int?,
val threeMonths: BigDecimal?,
@Json(name = "6m")
val sixMonths: Int?,
val sixMonths: BigDecimal?,
@Json(name = "1y")
val year: Int?,
val year: BigDecimal?,
@Json(name = "all_time")
val allTime: Int?,
val allTime: BigDecimal?,
)
data class Network(
@Json(name = "network_id")
val networkId: String,
@Json(name = "exchangeable")
val exchangeable: Boolean,
val exchangeable: Boolean = false,
@Json(name = "contract_address")
val contractAddress: String,
val contractAddress: String?,
@Json(name = "decimal_count")
val decimalCount: Int,
val decimalCount: Int?,
)
data class Insight(
data class Insights(
@Json(name = "holders_change")
val holdersChange: Change?,
@Json(name = "liquidity_change")
@ -71,11 +71,11 @@ data class TokenMarketInfoResponse(
data class Change(
@Json(name = "24h")
val day: Int?,
val day: BigDecimal?,
@Json(name = "1w")
val week: Int?,
val week: BigDecimal?,
@Json(name = "1m")
val month: Int?,
val month: BigDecimal?,
)
data class Metrics(
@ -123,9 +123,9 @@ data class TokenMarketInfoResponse(
)
data class Range(
@Json(name = "low")
val low: Int?,
@Json(name = "high")
val high: Int?,
@Json(name = "low_price")
val low: BigDecimal?,
@Json(name = "high_price")
val high: BigDecimal?,
)
}

View file

@ -30,6 +30,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
titleAction: TopAppBarButtonUM? = null,
containerColor: Color = TangemTheme.colors.background.primary,
addBottomInsets: Boolean = true,
skipPartiallyExpanded: Boolean = true,
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
TangemBottomSheet(
@ -37,6 +38,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
containerColor = containerColor,
addBottomInsets = addBottomInsets,
title = { TangemBottomSheetTitle(title = titleText, endButton = titleAction) },
skipPartiallyExpanded = skipPartiallyExpanded,
content = content,
)
}
@ -49,6 +51,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color = TangemTheme.colors.background.primary,
addBottomInsets: Boolean = true,
skipPartiallyExpanded: Boolean = true,
crossinline title: @Composable BoxScope.(T) -> Unit = {},
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
@ -61,6 +64,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
addBottomInsets = addBottomInsets,
title = title,
content = content,
skipPartiallyExpanded = skipPartiallyExpanded,
)
} else {
DefaultBottomSheet<T>(
@ -69,6 +73,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
addBottomInsets = addBottomInsets,
title = title,
content = content,
skipPartiallyExpanded = skipPartiallyExpanded,
)
}
}
@ -79,11 +84,12 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color,
addBottomInsets: Boolean,
skipPartiallyExpanded: Boolean = true,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
var isVisible by remember { mutableStateOf(value = config.isShow) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
if (isVisible && config.content is T) {
BasicBottomSheet<T>(
@ -111,6 +117,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color,
addBottomInsets: Boolean,
skipPartiallyExpanded: Boolean = true,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
@ -118,7 +125,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
modifier = Modifier.width(360.dp),
config = config,
sheetState = SheetState(
skipPartiallyExpanded = true,
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
density = LocalDensity.current,
),

View file

@ -1,10 +1,7 @@
package com.tangem.core.ui.res
import androidx.compose.animation.*
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.*
import androidx.compose.runtime.*
@Immutable
@ -24,10 +21,6 @@ object TangemAnimations {
@Immutable
object TransitionSpecs {
@Stable
val textChange: ContentTransform =
fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy)) togetherWith
fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy))
// TODO add more transition specs
}
}

View file

@ -117,6 +117,7 @@ object BigDecimalFormatter {
fiatAmount: BigDecimal?,
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
decimals: Int = FIAT_MARKET_DEFAULT_DIGITS,
locale: Locale = Locale.getDefault(),
): String {
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
@ -124,8 +125,8 @@ object BigDecimalFormatter {
val formatterCurrency = getCurrency(fiatCurrencyCode)
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
currency = formatterCurrency
maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS
maximumFractionDigits = decimals
minimumFractionDigits = decimals
roundingMode = RoundingMode.HALF_UP
}
@ -250,40 +251,27 @@ object BigDecimalFormatter {
/**
* "123456.6" -> "$123.457K"
* "12345.6" -> "$123.046K"
* Negative amount is not supported
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
* @param scale the number of digits to the right of the decimal point
*/
@Suppress("MagicNumber")
fun formatCompactAmount(
amount: BigDecimal,
fun formatCompactFiatAmount(
amount: BigDecimal?,
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
threeDigitsMethod: Boolean = false,
scale: Int = 0,
locale: Locale = Locale.getDefault(),
): String {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext(
amount = amount,
fiatCurrencyCode = fiatCurrencyCode,
fiatCurrencySymbol = fiatCurrencySymbol,
locale = locale,
)
}
if (amount == null) return EMPTY_BALANCE_SIGN
val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP)
val digitsCount = scaledAmount.longValueExact().toString().count()
val digitsToFormat = 6 - when (digitsCount % 3) {
0 -> 0
1 -> 2
else -> 1
}
val formatter = CompactDecimalFormat.getInstance(
locale,
CompactDecimalFormat.CompactStyle.SHORT,
).apply {
minimumSignificantDigits = 4
maximumSignificantDigits = digitsToFormat
}
val rawAmount = formatter.format(amount.setScale(0, RoundingMode.HALF_UP))
val rawAmount = formatCompactAmount(
amount = amount,
locale = locale,
threeDigitsMethod = threeDigitsMethod,
scale = scale,
)
return addCurrencySymbolToStringAmount(
amount = rawAmount,
@ -293,5 +281,53 @@ object BigDecimalFormatter {
)
}
/**
* "123456.6" -> "123.457K"
* "12345.6" -> "123.046K"
* Negative amount is not supported
* @param threeDigitsMethod if true, will format the amount always with 3 significant digits
* @param scale the number of digits to the right of the decimal point
*/
@Suppress("MagicNumber")
fun formatCompactAmount(
amount: BigDecimal,
locale: Locale = Locale.getDefault(),
threeDigitsMethod: Boolean = false,
scale: Int = 0,
): String {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext(amount = amount)
}
if (threeDigitsMethod) {
val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP)
val digitsCount = scaledAmount.longValueExact().toString().count()
val digitsToFormat = 6 - when (digitsCount % 3) {
0 -> 0
1 -> 2
else -> 1
}
val formatter = CompactDecimalFormat.getInstance(
locale,
CompactDecimalFormat.CompactStyle.SHORT,
).apply {
minimumSignificantDigits = 4
maximumSignificantDigits = digitsToFormat
}
return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP))
} else {
val value = amount.setScale(scale, RoundingMode.HALF_UP)
val formatter = CompactDecimalFormat.getInstance(
locale,
CompactDecimalFormat.CompactStyle.SHORT,
)
return formatter.format(value)
}
}
private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
}

View file

@ -7,16 +7,32 @@ import java.util.Locale
internal object BigDecimalFormatterCompat {
/**
* Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes
* Formats value as [BigDecimalFormatter.formatCompactFiatAmount] does using only "T","B","M","K" suffixes
* Used for < API24 compatibility
*/
@Suppress("MagicNumber", "UnnecessaryParentheses")
fun formatCompactAmountNoLocaleContext(
fun formatCompactFiatAmountNoLocaleContext(
amount: BigDecimal,
fiatCurrencyCode: String,
fiatCurrencySymbol: String,
locale: Locale = Locale.getDefault(),
): String {
val formatted = formatCompactAmountNoLocaleContext(amount)
return BigDecimalFormatter.addCurrencySymbolToStringAmount(
amount = formatted,
fiatCurrencyCode = fiatCurrencyCode,
fiatCurrencySymbol = fiatCurrencySymbol,
locale = locale,
)
}
/**
* Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes
* Used for < API24 compatibility
*/
@Suppress("MagicNumber", "UnnecessaryParentheses")
fun formatCompactAmountNoLocaleContext(amount: BigDecimal): String {
val value = amount.setScale(0, RoundingMode.HALF_UP).longValueExact()
val formatted = when {
@ -42,11 +58,6 @@ internal object BigDecimalFormatterCompat {
else -> return value.toString()
}
return BigDecimalFormatter.addCurrencySymbolToStringAmount(
amount = formatted,
fiatCurrencyCode = fiatCurrencyCode,
fiatCurrencySymbol = fiatCurrencySymbol,
locale = locale,
)
return formatted
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.core.ui.utils
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
fun Modifier.disableNestedScroll(): Modifier = nestedScroll(DisableParentConnection)
private object DisableParentConnection : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return available.copy(x = 0f)
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.utils
import java.util.Locale
object SupportedLanguages {
const val ENGLISH = "en"
const val RUSSIAN = "ru"
const val GERMAN = "de"
const val FRANCH = "fr"
const val ITALIAN = "it"
const val JAPANESE = "ja"
const val UKRAINIAN = "uk"
const val CHINESE = "uk"
val supportedLangugeCodes = listOf(
ENGLISH,
RUSSIAN,
GERMAN,
FRANCH,
ITALIAN,
JAPANESE,
UKRAINIAN,
CHINESE,
)
fun getCurrentSupportedLanguageCode(): String {
val locale = Locale.getDefault()
return if (supportedLangugeCodes.contains(locale.language)) {
locale.language
} else {
ENGLISH
}
}
}

View file

@ -108,10 +108,15 @@ internal class DefaultMarketsTokenRepository(
return tokenChartConverter.convert(interval, response.getOrThrow())
}
override suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo {
override suspend fun getTokenInfo(
fiatCurrencyCode: String,
tokenId: String,
languageCode: String,
): TokenMarketInfo {
val response = marketsApi.getCoinMarketData(
currency = fiatCurrencyCode,
coinId = tokenId,
language = languageCode,
)
return TokenMarketInfoConverter().convert(response.getOrThrow())

View file

@ -50,15 +50,13 @@ internal class TokenMarketInfoConverter : Converter<TokenMarketInfoResponse, Tok
}
@JvmName("convertInsight")
private fun List<TokenMarketInfoResponse.Insight>.convert(): List<TokenMarketInfo.Insight> {
return map {
TokenMarketInfo.Insight(
holdersChange = it.holdersChange?.convert(),
liquidityChange = it.liquidityChange?.convert(),
buyPressureChange = it.buyPressureChange?.convert(),
experiencedBuyerChange = it.experiencedBuyerChange?.convert(),
)
}
private fun TokenMarketInfoResponse.Insights.convert(): TokenMarketInfo.Insights {
return TokenMarketInfo.Insights(
holdersChange = holdersChange?.convert(),
liquidityChange = liquidityChange?.convert(),
buyPressureChange = buyPressureChange?.convert(),
experiencedBuyerChange = experiencedBuyerChange?.convert(),
)
}
private fun TokenMarketInfoResponse.Change.convert(): TokenMarketInfo.Change {

View file

@ -18,4 +18,5 @@ dependencies {
implementation(deps.kotlin.serialization)
implementation(projects.domain.tokens.models)
implementation(projects.core.utils)
}

View file

@ -11,29 +11,29 @@ data class TokenMarketInfo(
val networks: List<Network>?,
val shortDescription: String?,
val fullDescription: String?,
val insights: List<Insight>?,
val insights: Insights?,
val metrics: Metrics?,
val links: Links?,
val pricePerformance: PricePerformance?,
) {
data class PriceChangePercentage(
val day: Int?,
val week: Int?,
val month: Int?,
val threeMonths: Int?,
val sixMonths: Int?,
val year: Int?,
val allTime: Int?,
val day: BigDecimal?,
val week: BigDecimal?,
val month: BigDecimal?,
val threeMonths: BigDecimal?,
val sixMonths: BigDecimal?,
val year: BigDecimal?,
val allTime: BigDecimal?,
)
data class Network(
val networkId: String,
val exchangeable: Boolean,
val contractAddress: String,
val decimalCount: Int,
val contractAddress: String?,
val decimalCount: Int?,
)
data class Insight(
data class Insights(
val holdersChange: Change?,
val liquidityChange: Change?,
val buyPressureChange: Change?,
@ -41,9 +41,9 @@ data class TokenMarketInfo(
)
data class Change(
val day: Int?,
val week: Int?,
val month: Int?,
val day: BigDecimal?,
val week: BigDecimal?,
val month: BigDecimal?,
)
data class Metrics(
@ -75,7 +75,7 @@ data class TokenMarketInfo(
)
data class Range(
val low: Int?,
val high: Int?,
val low: BigDecimal?,
val high: BigDecimal?,
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.domain.markets
import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.utils.SupportedLanguages
class GetTokenMarketInfoUseCase(
private val marketsTokenRepository: MarketsTokenRepository,
@ -13,6 +14,7 @@ class GetTokenMarketInfoUseCase(
marketsTokenRepository.getTokenInfo(
fiatCurrencyCode = appCurrency.code,
tokenId = tokenId,
languageCode = SupportedLanguages.getCurrentSupportedLanguageCode(),
)
}.mapLeft {}
}

View file

@ -12,5 +12,5 @@ interface MarketsTokenRepository {
suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String): TokenMarketInfo
suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String, languageCode: String): TokenMarketInfo
}

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