Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-14 12:52:27 +03:00
parent c6d333dbeb
commit 244f9bc61d
19 changed files with 224 additions and 77 deletions

View file

@ -314,6 +314,7 @@ private fun MarketChartPreview(
TangemThemePreview {
val growingColor = TangemTheme.colors.icon.accent
val fallingColor = TangemTheme.colors.icon.warning
val neutralColor = TangemTheme.colors.icon.informative
val chartState = rememberMarketChartState(
dataProducer = dataProducer,
@ -324,6 +325,7 @@ private fun MarketChartPreview(
when (it) {
MarketChartLook.Type.Growing -> growingColor
MarketChartLook.Type.Falling -> fallingColor
MarketChartLook.Type.Neutral -> neutralColor
}
},
)
@ -382,10 +384,10 @@ private fun MarketChartPreview(
dataProducer.runTransaction {
updateLook {
it.copy(
type = if (it.type == MarketChartLook.Type.Growing) {
MarketChartLook.Type.Falling
} else {
MarketChartLook.Type.Growing
type = when (it.type) {
MarketChartLook.Type.Growing -> MarketChartLook.Type.Falling
MarketChartLook.Type.Falling -> MarketChartLook.Type.Neutral
MarketChartLook.Type.Neutral -> MarketChartLook.Type.Growing
},
)
}

View file

@ -36,6 +36,7 @@ fun MarketChartMini(
type: MarketChartLook.Type = MarketChartLook.Type.Growing,
growingColor: Color = TangemTheme.colors.icon.accent,
fallingColor: Color = TangemTheme.colors.icon.warning,
neutralColor: Color = TangemTheme.colors.icon.informative,
) {
val model = remember(rawData) {
CartesianChartModel(LineCartesianLayerModel.build { series(rawData.y) })
@ -44,6 +45,7 @@ fun MarketChartMini(
val lineColor = when (type) {
MarketChartLook.Type.Growing -> growingColor
MarketChartLook.Type.Falling -> fallingColor
MarketChartLook.Type.Neutral -> neutralColor
}
val lineSpec = rememberLine(
@ -85,6 +87,8 @@ private fun Preview() {
MarketChartMini(rawData = data, type = MarketChartLook.Type.Growing)
SpacerH16()
MarketChartMini(rawData = data, type = MarketChartLook.Type.Falling)
SpacerH16()
MarketChartMini(rawData = data, type = MarketChartLook.Type.Neutral)
}
}
}

View file

@ -24,5 +24,6 @@ data class MarketChartLook(
enum class Type {
Growing,
Falling,
Neutral,
}
}

View file

@ -24,6 +24,7 @@ fun rememberMarketChartState(
when (it) {
MarketChartLook.Type.Growing -> Color.Green
MarketChartLook.Type.Falling -> Color.Red
MarketChartLook.Type.Neutral -> Color.Gray
}
}
},

View file

@ -10,13 +10,22 @@ import java.util.Locale
@Suppress("MagicNumber")
object DateTimeFormatters {
/**
* Determine if the time is in 12-hour format ("10:00 PM") for the current locale.
*/
val is12HourFormat by lazy {
/**
* Two SS means, SHORT style for date and time.
* If pattern contains "a", it means time is in 12 hour format.
* [Documentation](https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html)
*/
DateTimeFormat.patternForStyle("SS", Locale.getDefault()).contains("a")
}
/**
* Example: "12:00 PM", "12:00"
*/
val timeFormatter: DateTimeFormatter by lazy {
val is12HourFormat = DateTimeFormat.patternForStyle("SS", Locale.getDefault()).contains("a")
if (is12HourFormat) {
DateTimeFormatterBuilder()
.appendClockhourOfHalfday(1)
@ -36,6 +45,9 @@ object DateTimeFormatters {
}
}
/**
* Example: "1 Jun, 2020", "1 Jun, 2020"
*/
val dateFormatter: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendDayOfMonth(1)
@ -47,36 +59,61 @@ object DateTimeFormatters {
.withLocale(Locale.getDefault())
}
/**
* Example: "31.06.2020", "06/31/2020"
*/
val dateDDMMYYYY: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendPattern("dd.MM.yyyy")
.toFormatter()
.withLocale(Locale.getDefault())
getBestFormatterBySkeleton("dd.MM.yyyy")
}
/**
* In API version < 24, there may be some problems with getting the best date and time format pattern.
* Example: "Jun 31, 2020", "31 Jun, 2020"
*/
val dateMMMdd: DateTimeFormatter by lazy {
getBestFormatterBySkeleton("dd MMM")
getBestFormatterBySkeleton("MMM dd")
}
/**
* Example: "2020"
*/
val dateYYYY: DateTimeFormatter by lazy {
getBestFormatterBySkeleton("yyyy")
}
/**
* Example: "31.06.2020 12:00", "06/31/2020 12:00", "06/31/2020 12:00 PM"
*/
val dateTimeFormatter: DateTimeFormatter by lazy {
DateTimeFormat.forPattern("dd.MM.yyyy HH:mm")
getBestFormatterBySkeleton("dd.MM.yyyy HH:mm")
}
fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String {
return formatter.print(date)
}
/**
* Returns the best date and time format pattern for the given skeleton and the current locale.
* (In API version < 24, there may be some problems with getting the best date and time format pattern.)
*
* @param skeleton The skeleton is an alternative to the pattern. The difference is that the pattern rigidly
* defines the date/time format, while the skeleton specifies only the date/time components (year, month, day, etc.)
* So the order of the components and separators (space, comma, etc.) is not taken into account.
* @see [dateYYYY], [dateMMMdd], [dateDDMMYYYY], [dateTimeFormatter]
*/
fun getBestFormatterBySkeleton(skeleton: String): DateTimeFormatter {
val skeletonWithLocale = skeleton.replaceHourLetters()
return DateTimeFormatterBuilder()
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton))
.appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), skeletonWithLocale))
.toFormatter()
.withLocale(Locale.getDefault())
}
private fun String.replaceHourLetters(): String {
return if (is12HourFormat) {
this.replace('H', 'h').replace('k', 'K')
} else {
this.replace('h', 'H').replace('K', 'k')
}
}
}

View file

@ -87,8 +87,27 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
}
}
// order of LaunchedEffects is important here
val activeChild = stackState.value.active.configuration
LaunchedEffect(activeChild) {
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.animateTo(
secondary,
animationSpec = tween(durationMillis = 500),
)
}
MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.animateTo(
primary,
animationSpec = tween(durationMillis = 500),
)
}
}
}
LaunchedEffect(bottomSheetState.value) {
if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) {
when (bottomSheetState.value) {
@ -108,23 +127,6 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
}
}
LaunchedEffect(activeChild) {
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.animateTo(
secondary,
animationSpec = tween(durationMillis = 500),
)
}
MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.animateTo(
primary,
animationSpec = tween(durationMillis = 500),
)
}
}
}
LaunchedEffect(primary, secondary) {
if (backgroundColor.isRunning) return@LaunchedEffect

View file

@ -291,6 +291,14 @@ internal class MarketsTokenDetailsModel @Inject constructor(
),
)
}
chartDataProducer.runTransaction {
updateLook {
it.copy(
type = getChartTypeByPercent(percent),
)
}
}
},
ifLeft = {
state.update {
@ -347,6 +355,14 @@ internal class MarketsTokenDetailsModel @Inject constructor(
dateTimeText = getDefaultDateTimeString(stateToUpdate.selectedInterval),
)
}
chartDataProducer.runTransaction {
updateLook {
it.copy(
type = getChartTypeByPercent(percent),
)
}
}
}
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {

View file

@ -2,6 +2,7 @@ package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketInfo
@ -43,6 +44,14 @@ internal class InsightsConverter(
liquidityChange = liquidityChange?.month,
buyPressureChange = buyPressureChange?.month,
),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
title = resourceReference(R.string.markets_token_details_insights),
body = stringReference("//TODO"),
),
)
},
)
}
}
@ -57,6 +66,7 @@ internal class InsightsConverter(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_experienced_buyers),
value = experiencedBuyerChange.convertChange(),
change = experiencedBuyerChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -69,6 +79,7 @@ internal class InsightsConverter(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_buy_pressure),
value = buyPressureChange.convertChange(isFiatValue = true),
change = buyPressureChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -81,6 +92,7 @@ internal class InsightsConverter(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_holders),
value = holdersChange.convertChange(),
change = holdersChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -93,6 +105,7 @@ internal class InsightsConverter(
InfoPointUM(
title = resourceReference(R.string.markets_token_details_liquidity),
value = liquidityChange.convertChange(),
change = liquidityChange.changeType(),
onInfoClick = {
onInfoClick(
InfoBottomSheetContent(
@ -105,6 +118,15 @@ internal class InsightsConverter(
)
}
private fun BigDecimal?.changeType(): InfoPointUM.ChangeType? {
return when {
this == null -> null
this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP
this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN
else -> null
}
}
private fun BigDecimal?.convertChange(isFiatValue: Boolean = false): String {
if (this == null) return StringsSigns.DASH_SIGN

View file

@ -83,16 +83,17 @@ internal fun PriceChangeType.toChartType(): MarketChartLook.Type {
return when (this) {
PriceChangeType.UP -> MarketChartLook.Type.Growing
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Growing
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral
}
}
@Suppress("MagicNumber")
internal fun getChartTypeByPercent(percent: BigDecimal?): MarketChartLook.Type {
val scaled = percent?.setScale(4, RoundingMode.HALF_UP)
return when {
percent == null -> return MarketChartLook.Type.Growing
percent == BigDecimal.ZERO -> MarketChartLook.Type.Growing
percent > BigDecimal.ZERO -> MarketChartLook.Type.Growing
percent < BigDecimal.ZERO -> MarketChartLook.Type.Falling
else -> MarketChartLook.Type.Growing
scaled == null -> return MarketChartLook.Type.Neutral
scaled > BigDecimal.ZERO -> MarketChartLook.Type.Growing
scaled < BigDecimal.ZERO -> MarketChartLook.Type.Falling
else -> MarketChartLook.Type.Neutral
}
}

View file

@ -18,7 +18,7 @@ internal object MarketsDateTimeFormatters {
private const val WEEK_MILLIS = 7L * H24_MILLIS
private val dateTimeMMMFormatter by lazy {
DateTimeFormatters.getBestFormatterBySkeleton("dd MMM hh:mm")
DateTimeFormatters.getBestFormatterBySkeleton("dd MMM Hm")
}
private val dateFormatter = DateTimeFormatters.dateDDMMYYYY

View file

@ -3,13 +3,18 @@ package com.tangem.features.markets.details.impl.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.text.TooltipText
import com.tangem.core.ui.extensions.resolveReference
@ -40,11 +45,32 @@ internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier)
overflow = TextOverflow.Ellipsis,
)
}
Row {
Text(
text = infoPointUM.value,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
if (infoPointUM.change != null) {
SpacerW4()
Icon(
modifier = Modifier
.size(TangemTheme.dimens.size8)
.align(Alignment.CenterVertically),
imageVector = ImageVector.vectorResource(
id = when (infoPointUM.change) {
InfoPointUM.ChangeType.UP -> R.drawable.ic_arrow_up_8
InfoPointUM.ChangeType.DOWN -> R.drawable.ic_arrow_down_8
},
),
tint = when (infoPointUM.change) {
InfoPointUM.ChangeType.UP -> TangemTheme.colors.icon.accent
InfoPointUM.ChangeType.DOWN -> TangemTheme.colors.icon.warning
},
contentDescription = null,
)
}
}
}
}
@ -105,6 +131,22 @@ private fun ContentPreview() {
onInfoClick = { },
),
)
InfoPoint(
infoPointUM = InfoPointUM(
title = stringReference("Market Cap"),
value = "$1,000,000",
change = InfoPointUM.ChangeType.UP,
onInfoClick = { },
),
)
InfoPoint(
infoPointUM = InfoPointUM(
title = stringReference("Market Cap"),
value = "$1,000,000",
change = InfoPointUM.ChangeType.DOWN,
onInfoClick = { },
),
)
}
}
}
@ -126,6 +168,14 @@ private fun PreviewShimmer() {
modifier = Modifier.fillMaxWidth(),
withTooltip = true,
)
InfoPointShimmer(
modifier = Modifier.fillMaxWidth(),
withTooltip = true,
)
InfoPointShimmer(
modifier = Modifier.fillMaxWidth(),
withTooltip = true,
)
}
},
actualContent = {

View file

@ -7,13 +7,12 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.block.information.GridItems
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.text.TooltipText
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
@ -34,13 +33,10 @@ internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
title = {
Text(
modifier = Modifier,
text = stringResource(R.string.markets_token_details_insights),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
TooltipText(
text = resourceReference(R.string.markets_token_details_insights),
textStyle = TangemTheme.typography.subtitle2,
onInfoClick = state.onInfoClick,
)
},
action = {
@ -180,6 +176,7 @@ private fun ContentPreview() {
value = "1 000",
),
),
onInfoClick = {},
),
)
}

View file

@ -22,6 +22,7 @@ import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) {
val growingColor = TangemTheme.colors.icon.accent
val fallingColor = TangemTheme.colors.icon.warning
val neutralColor = TangemTheme.colors.icon.informative
val chartState = rememberMarketChartState(
dataProducer = state.dataProducer,
@ -29,6 +30,7 @@ internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, mo
when (it) {
MarketChartLook.Type.Growing -> growingColor
MarketChartLook.Type.Falling -> fallingColor
MarketChartLook.Type.Neutral -> neutralColor
}
},
onMarkerShown = state.onMarkerPointSelected,

View file

@ -5,5 +5,10 @@ import com.tangem.core.ui.extensions.TextReference
internal data class InfoPointUM(
val title: TextReference,
val value: String,
val change: ChangeType? = null,
val onInfoClick: (() -> Unit)? = null,
)
) {
enum class ChangeType {
UP, DOWN
}
}

View file

@ -6,4 +6,5 @@ internal data class InsightsUM(
val h24Info: ImmutableList<InfoPointUM>,
val weekInfo: ImmutableList<InfoPointUM>,
val monthInfo: ImmutableList<InfoPointUM>,
val onInfoClick: () -> Unit,
)

View file

@ -48,6 +48,7 @@ internal class MarketsListModel @Inject constructor(
private val visibleItemIds = MutableStateFlow<List<String>>(emptyList())
private val marketsListUMStateManager = MarketsListUMStateManager(
currentVisibleIds = Provider { visibleItemIds.value },
onLoadMoreUiItems = { activeListManager.loadMore() },
visibleItemsChanged = { visibleItemIds.value = it },
onRetryButtonClicked = { activeListManager.reload() },

View file

@ -12,6 +12,7 @@ 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 com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.*
@Stable
internal class MarketsListUMStateManager(
private val currentVisibleIds: Provider<List<String>>,
private val onLoadMoreUiItems: () -> Unit,
private val visibleItemsChanged: (itemsKeys: List<String>) -> Unit,
private val onRetryButtonClicked: () -> Unit,
@ -129,9 +131,11 @@ internal class MarketsListUMStateManager(
}
}
val itemsWithFilteredPriceChange = items.filterPriceChangeByVisibility()
return currentState.copy(
list = ListUM.Content(
items = items,
items = itemsWithFilteredPriceChange,
loadMore = onLoadMoreUiItems,
visibleIdsChanged = visibleItemsChanged,
showUnder100kTokens = isInSearchState.not() || isNextPageInSearch,
@ -157,6 +161,22 @@ internal class MarketsListUMStateManager(
)
}
// Show price change animation for visible items only
private fun ImmutableList<MarketsListItemUM>.filterPriceChangeByVisibility(): ImmutableList<MarketsListItemUM> {
val visibleItemIds = currentVisibleIds()
return map {
it.copy(
price = it.price.copy(
changeType = if (visibleItemIds.contains(it.id)) {
it.price.changeType
} else {
null
},
),
)
}.toImmutableList()
}
private fun generalContentState(newItems: ImmutableList<MarketsListItemUM>): ListUM.Content {
return ListUM.Content(
items = newItems,

View file

@ -34,11 +34,11 @@ fun MarketsListItemPlaceholder() {
SpacerW12()
Column(modifier = Modifier.weight(1f)) {
Row(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing4),
horizontalArrangement = Arrangement.SpaceBetween,
contentAlignment = Alignment.CenterStart,
) {
RectangleShimmer(
modifier = Modifier
@ -46,23 +46,15 @@ fun MarketsListItemPlaceholder() {
.height(sp12),
radius = TangemTheme.dimens.radius3,
)
SpacerW8()
RectangleShimmer(
modifier = Modifier
.width(TangemTheme.dimens.size70)
.height(sp12),
radius = TangemTheme.dimens.radius3,
)
}
SpacerH(height = TangemTheme.dimens.spacing2)
Row(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing2),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Bottom,
contentAlignment = Alignment.CenterStart,
) {
RectangleShimmer(
modifier = Modifier
@ -70,12 +62,6 @@ fun MarketsListItemPlaceholder() {
.height(sp12),
radius = TangemTheme.dimens.radius3,
)
RectangleShimmer(
modifier = Modifier
.width(TangemTheme.dimens.size52)
.height(sp12),
radius = TangemTheme.dimens.radius3,
)
}
}

View file

@ -20,10 +20,9 @@ data class MarketsListItemUM(
val isUnder100kMarketCap: Boolean = false,
) {
val chartType: MarketChartLook.Type = when (trendType) {
PriceChangeType.UP,
PriceChangeType.NEUTRAL,
-> MarketChartLook.Type.Growing
PriceChangeType.UP -> MarketChartLook.Type.Growing
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral
}
@Immutable