diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt
index 92785ec693..dc99632334 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt
@@ -121,6 +121,7 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) {
PriceChangeInPercent(
valueInPercent = marketPriceBlockState.priceChangeConfig.valueInPercent,
type = marketPriceBlockState.priceChangeConfig.type,
+ textStyle = TangemTheme.typography.body2,
)
}
} else {
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt
index a202d8bf21..c4882dcdc8 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@@ -13,15 +14,43 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
+import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
+import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
fun PriceChangeInPercent(
valueInPercent: String,
type: PriceChangeType,
+ textStyle: TextStyle,
+ modifier: Modifier = Modifier,
+ isDisabled: Boolean = false,
+) {
+ if (LocalRedesignEnabled.current) {
+ PriceChangeInPercentV2(
+ modifier = modifier,
+ valueInPercent = valueInPercent,
+ type = type,
+ textStyle = textStyle,
+ isDisabled = isDisabled,
+ )
+ } else {
+ PriceChangeInPercentV1(
+ modifier = modifier,
+ valueInPercent = valueInPercent,
+ type = type,
+ textStyle = textStyle,
+ )
+ }
+}
+
+@Composable
+private fun PriceChangeInPercentV1(
+ valueInPercent: String,
+ type: PriceChangeType,
+ textStyle: TextStyle,
modifier: Modifier = Modifier,
- textStyle: TextStyle = TangemTheme.typography.body2,
) {
if (valueInPercent.isBlank()) {
Box(modifier)
@@ -66,25 +95,87 @@ fun PriceChangeInPercent(
}
}
+@Composable
+private fun PriceChangeInPercentV2(
+ valueInPercent: String,
+ type: PriceChangeType,
+ textStyle: TextStyle,
+ isDisabled: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ if (valueInPercent.isBlank()) {
+ Box(modifier)
+ return
+ }
+
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens2.x0_5),
+ ) {
+ Icon(
+ modifier = Modifier
+ .size(TangemTheme.dimens2.x3)
+ .align(Alignment.CenterVertically),
+ imageVector = ImageVector.vectorResource(
+ id = when (type) {
+ PriceChangeType.UP -> R.drawable.ic_arrow_up_8
+ PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8
+ PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8
+ },
+ ),
+ tint = if (isDisabled) {
+ TangemTheme.colors2.graphic.neutral.tertiary
+ } else {
+ when (type) {
+ PriceChangeType.UP -> TangemTheme.colors2.markers.iconBlue
+ PriceChangeType.DOWN -> TangemTheme.colors2.markers.iconRed
+ PriceChangeType.NEUTRAL -> TangemTheme.colors2.markers.iconGray
+ }
+ },
+ contentDescription = null,
+ )
+
+ Text(
+ text = valueInPercent,
+ color = if (isDisabled) {
+ TangemTheme.colors2.text.status.disabled
+ } else {
+ when (type) {
+ PriceChangeType.UP -> TangemTheme.colors2.text.status.accent
+ PriceChangeType.DOWN -> TangemTheme.colors2.text.status.warning
+ PriceChangeType.NEUTRAL -> TangemTheme.colors2.text.neutral.tertiary
+ }
+ },
+ style = textStyle,
+ overflow = TextOverflow.Visible,
+ maxLines = 1,
+ )
+ }
+}
+
//region Preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
-private fun Preview() {
+private fun PreviewV1() {
TangemThemePreview {
Column {
PriceChangeInPercent(
valueInPercent = "52.00%",
type = PriceChangeType.NEUTRAL,
+ textStyle = TangemTheme.typography.body2,
)
PriceChangeInPercent(
valueInPercent = "52.00%",
type = PriceChangeType.UP,
+ textStyle = TangemTheme.typography.body2,
)
PriceChangeInPercent(
valueInPercent = "52.00%",
type = PriceChangeType.DOWN,
+ textStyle = TangemTheme.typography.body2,
)
PriceChangeInPercent(
valueInPercent = "52.00%",
@@ -95,4 +186,38 @@ private fun Preview() {
}
}
+@Preview
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun PreviewV2() {
+ TangemThemePreviewRedesign {
+ CompositionLocalProvider(LocalRedesignEnabled provides true) {
+ Column {
+ PriceChangeInPercent(
+ valueInPercent = "52.00%",
+ type = PriceChangeType.NEUTRAL,
+ textStyle = TangemTheme.typography2.captionRegular12,
+ )
+ PriceChangeInPercent(
+ valueInPercent = "52.00%",
+ type = PriceChangeType.UP,
+ textStyle = TangemTheme.typography2.captionRegular12,
+ isDisabled = true,
+ )
+ PriceChangeInPercent(
+ valueInPercent = "52.00%",
+ type = PriceChangeType.DOWN,
+ textStyle = TangemTheme.typography2.captionRegular12,
+ isDisabled = false,
+ )
+ PriceChangeInPercent(
+ valueInPercent = "52.00%",
+ type = PriceChangeType.DOWN,
+ textStyle = TangemTheme.typography2.captionRegular12,
+ )
+ }
+ }
+ }
+}
+
//endregion Preview
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt
index 8db483f50c..768c0696cc 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt
@@ -1,8 +1,11 @@
package com.tangem.core.ui.components.marketprice
-sealed class PriceChangeState {
+import androidx.compose.runtime.Immutable
- data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState()
+@Immutable
+sealed interface PriceChangeState {
- object Unknown : PriceChangeState()
+ data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState
+
+ data object Unknown : PriceChangeState
}
\ No newline at end of file
diff --git a/core/ui/src/main/res/drawable/ic_alert_triange_24.xml b/core/ui/src/main/res/drawable/ic_alert_triange_24.xml
new file mode 100644
index 0000000000..6442020ee2
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_alert_triange_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_chewron_up_20.xml b/core/ui/src/main/res/drawable/ic_chewron_up_20.xml
new file mode 100644
index 0000000000..aeb0f01317
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_chewron_up_20.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt
index f43e8c5766..c55b8f0ec8 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt
@@ -1,6 +1,8 @@
package com.tangem.features.feed.model.search
import arrow.core.getOrElse
+import com.tangem.common.routing.AppRoute
+import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.charts.state.MarketChartData
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
import com.tangem.common.ui.charts.state.sorted
@@ -19,6 +21,7 @@ import com.tangem.domain.markets.toSerializableParam
import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase
import com.tangem.domain.search.usecase.GetSearchResultsUseCase
import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase
+import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.domain.search.usecase.SaveSearchQueryUseCase
import com.tangem.features.feed.components.search.DefaultSearchComponent
import com.tangem.features.feed.model.market.list.state.MarketsListUM
@@ -58,6 +61,7 @@ internal class SearchModel @Inject constructor(
private val saveRecentSearchTokenUseCase: SaveRecentSearchTokenUseCase,
private val clearSearchHistoryUseCase: ClearSearchHistoryUseCase,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
+ private val appRouter: AppRouter,
private val stateController: SearchStateController,
) : Model() {
@@ -195,6 +199,15 @@ internal class SearchModel @Inject constructor(
stateController.update(UpdateSearchBarQueryTransformer(""))
}
+ private fun onSingleUserAssetClick(entry: UserAssetSearchEntry) {
+ appRouter.push(
+ AppRoute.CurrencyDetails(
+ userWalletId = entry.userWalletId,
+ currency = entry.currencyStatus.currency,
+ ),
+ )
+ }
+
private fun subscribeToQueryChanges() {
stateController.uiState
.map { it.searchBar.query.trim() }
@@ -231,6 +244,7 @@ internal class SearchModel @Inject constructor(
val converter = UserAssetSearchItemConverter(
appCurrency = appCurrency,
isBalanceHidden = balanceHidden,
+ onSingleClick = ::onSingleUserAssetClick,
)
searchResult.userAssets
.map(converter::convert)
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt
index 4e742afdde..6f0134ca29 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt
@@ -1,23 +1,34 @@
package com.tangem.features.feed.model.search.converter
import com.tangem.common.ui.account.toUM
+import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated
+import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
+import com.tangem.core.ui.components.marketprice.PriceChangeState
+import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.extensions.networkIconResId
+import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
+import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency
+import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.domain.search.model.UserAssetSearchItem
+import com.tangem.features.feed.ui.search.state.BalanceDisplayState
import com.tangem.features.feed.ui.search.state.UserAssetItemUM
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
+import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
internal class UserAssetSearchItemConverter(
private val appCurrency: AppCurrency,
private val isBalanceHidden: Boolean,
+ private val onSingleClick: (UserAssetSearchEntry) -> Unit,
) : Converter {
override fun convert(value: UserAssetSearchItem): UserAssetItemUM {
@@ -39,17 +50,56 @@ internal class UserAssetSearchItemConverter(
tokenName = currency.name,
tokenSymbol = currency.symbol,
fiatRate = value.fiatRate?.format { fiat(appCurrency.code, appCurrency.symbol) },
- cryptoBalance = formatCryptoAmount(value.amount, currency.symbol, currency.decimals),
- fiatBalance = formatFiatAmount(value.fiatAmount),
+ priceChangeState = when (value) {
+ is CryptoCurrencyStatus.Loading,
+ is CryptoCurrencyStatus.Unreachable,
+ is CryptoCurrencyStatus.MissedDerivation,
+ is CryptoCurrencyStatus.NoAmount,
+ -> PriceChangeState.Unknown
+ else -> PriceChangeState.Content(
+ type = PriceChangeType.fromBigDecimal(value.priceChange.orZero()),
+ valueInPercent = value.priceChange.format { percent() },
+ )
+ },
+ balanceState = convertSingleBalanceState(value, currency.symbol, currency.decimals),
isBalanceHidden = isBalanceHidden,
- onClick = {},
+ onClick = { onSingleClick(entry) },
)
}
- private fun convertGrouped(item: UserAssetSearchItem.Grouped): UserAssetItemUM.Grouped {
- val totalFiat = item.entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
- val totalCrypto = item.entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO }
+ private fun convertSingleBalanceState(
+ value: CryptoCurrencyStatus.Value,
+ symbol: String,
+ decimals: Int,
+ ): BalanceDisplayState {
+ return when {
+ value is CryptoCurrencyStatus.Loading && value.amount != null ->
+ BalanceDisplayState.Flickering(
+ cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)),
+ fiatBalance = value.fiatAmount?.toMarketsListItemPriceAnnotated(
+ appCurrencyCode = appCurrency.code, appCurrencySymbol = appCurrency.symbol,
+ ) ?: stringReference(StringsSigns.DASH_SIGN),
+ )
+ value is CryptoCurrencyStatus.Loading -> BalanceDisplayState.Loading
+ value is CryptoCurrencyStatus.Unreachable -> BalanceDisplayState.Unreachable
+ value.isError && value.amount != null ->
+ BalanceDisplayState.Stale(
+ cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)),
+ fiatBalance = value.fiatAmount?.toMarketsListItemPriceAnnotated(
+ appCurrencyCode = appCurrency.code, appCurrencySymbol = appCurrency.symbol,
+ ) ?: stringReference(StringsSigns.DASH_SIGN),
+ )
+ value.isError -> BalanceDisplayState.Unreachable
+ else -> BalanceDisplayState.Loaded(
+ cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)),
+ fiatBalance = value.fiatAmount?.toMarketsListItemPriceAnnotated(
+ appCurrencyCode = appCurrency.code, appCurrencySymbol = appCurrency.symbol,
+ ) ?: stringReference(StringsSigns.DASH_SIGN),
+ )
+ }
+ }
+ private fun convertGrouped(item: UserAssetSearchItem.Grouped): UserAssetItemUM.Grouped {
val firstCurrency = item.entries.first().currencyStatus.currency
val children = item.entries.map { entry ->
UserAssetItemUM.GroupedChild(
@@ -57,36 +107,78 @@ internal class UserAssetSearchItemConverter(
accountName = entry.accountName.toUM(),
accountIcon = entry.accountIcon.value,
accountColor = entry.accountIcon.color,
- cryptoBalance = formatCryptoAmount(
- entry.currencyStatus.value.amount,
- entry.currencyStatus.currency.symbol,
- entry.currencyStatus.currency.decimals,
- ),
- fiatBalance = formatFiatAmount(entry.currencyStatus.value.fiatAmount),
+ currencyStatus = entry.currencyStatus,
)
}.toImmutableList()
+ val entryCurrencyStatus = item.entries.first().currencyStatus
+
return UserAssetItemUM.Grouped(
id = "grouped_${item.tokenName}_${item.tokenSymbol}",
icon = TangemIconUM.Currency(
- currencyIconState = CryptoCurrencyToIconStateConverter().convert(item.entries.first().currencyStatus),
+ currencyIconState = CurrencyIconState.CoinIcon(
+ url = entryCurrencyStatus.currency.iconUrl,
+ fallbackResId = entryCurrencyStatus.currency.networkIconResId,
+ isGrayscale = entryCurrencyStatus.currency.network.isTestnet || entryCurrencyStatus.value.isError,
+ shouldShowCustomBadge = entryCurrencyStatus.currency.isCustom,
+ ),
),
tokenName = item.tokenName,
tokenSymbol = item.tokenSymbol,
tokensCount = item.entries.size,
- totalCryptoBalance = formatCryptoAmount(totalCrypto, firstCurrency.symbol, firstCurrency.decimals),
- totalFiatBalance = formatFiatAmount(totalFiat),
+ balanceState = convertGroupedBalanceState(item.entries, firstCurrency.symbol, firstCurrency.decimals),
isBalanceHidden = isBalanceHidden,
children = children,
onClick = {},
)
}
+ private fun convertGroupedBalanceState(
+ entries: List,
+ symbol: String,
+ decimals: Int,
+ ): BalanceDisplayState {
+ val hasAnyLoading = entries.any { it.currencyStatus.value is CryptoCurrencyStatus.Loading }
+ val hasAnyError = entries.any { it.currencyStatus.value.isError }
+ val hasAnyAmount = entries.any { it.currencyStatus.value.amount != null }
+
+ val balance = when {
+ hasAnyLoading && !hasAnyAmount -> BalanceDisplayState.Loading
+ hasAnyLoading && hasAnyAmount -> computeGroupBalanceFlickering(entries, symbol, decimals)
+ hasAnyError && entries.size == 1 && !hasAnyAmount -> BalanceDisplayState.Unreachable
+ hasAnyError && entries.size > 1 -> computeGroupBalance(entries, symbol, decimals)
+ else -> computeGroupBalance(entries, symbol, decimals)
+ }
+ return balance
+ }
+
+ private fun computeGroupBalance(
+ entries: List,
+ symbol: String,
+ decimals: Int,
+ ): BalanceDisplayState.Loaded {
+ val totalFiat = entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
+ val totalCrypto = entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO }
+ return BalanceDisplayState.Loaded(
+ cryptoBalance = stringReference(formatCryptoAmount(totalCrypto, symbol, decimals)),
+ fiatBalance = totalFiat.toMarketsListItemPriceAnnotated(appCurrency.code, appCurrency.symbol),
+ )
+ }
+
+ private fun computeGroupBalanceFlickering(
+ entries: List,
+ symbol: String,
+ decimals: Int,
+ ): BalanceDisplayState.Flickering {
+ val totalFiat = entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO }
+ val totalCrypto = entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO }
+ return BalanceDisplayState.Flickering(
+ cryptoBalance = stringReference(formatCryptoAmount(totalCrypto, symbol, decimals)),
+ fiatBalance = totalFiat.toMarketsListItemPriceAnnotated(appCurrency.code, appCurrency.symbol),
+ )
+ }
+
private fun formatCryptoAmount(amount: BigDecimal?, symbol: String, decimals: Int): String {
return amount?.format { crypto(symbol, decimals) } ?: StringsSigns.DASH_SIGN
}
-
- private fun formatFiatAmount(fiatAmount: BigDecimal?): String {
- return fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) } ?: StringsSigns.DASH_SIGN
- }
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/LayeringIcons.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/LayeringIcons.kt
new file mode 100644
index 0000000000..1236344817
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/LayeringIcons.kt
@@ -0,0 +1,99 @@
+package com.tangem.features.feed.ui.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import com.tangem.core.ui.components.currency.icon.CurrencyIconState
+import com.tangem.core.ui.ds.image.TangemIcon
+import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.res.TangemColorPalette
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreview
+
+private const val MIN_STACKED_ICON_COUNT = 1
+private const val MAX_STACKED_ICON_COUNT = 3
+private const val FIRST_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER = 1
+private const val SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER = 2
+private const val MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER = 2
+private const val FIRST_BACK_LAYER_ICON_ALPHA = 0.4f
+private const val SECOND_BACK_LAYER_ICON_ALPHA = 0.2f
+
+@Composable
+fun LayeringIcons(
+ tangemIconUM: TangemIconUM,
+ modifier: Modifier = Modifier,
+ count: Int = MIN_STACKED_ICON_COUNT,
+ layerHorizontalShift: Dp = TangemTheme.dimens2.x1,
+ iconSize: Dp = TangemTheme.dimens2.x10,
+) {
+ require(count > 0)
+
+ val stackTrailingWidth = layerHorizontalShift * SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER
+
+ Box(
+ modifier = modifier.size(
+ width = iconSize + stackTrailingWidth,
+ height = iconSize,
+ ),
+ ) {
+ val baseIconModifier = Modifier
+ .align(Alignment.TopStart)
+ .size(iconSize)
+
+ if (count >= MAX_STACKED_ICON_COUNT) {
+ TangemIcon(
+ tangemIconUM = tangemIconUM,
+ modifier = baseIconModifier
+ .offset(x = layerHorizontalShift * SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER)
+ .alpha(SECOND_BACK_LAYER_ICON_ALPHA),
+ )
+ }
+ if (count >= MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER) {
+ TangemIcon(
+ tangemIconUM = tangemIconUM,
+ modifier = baseIconModifier
+ .offset(x = layerHorizontalShift * FIRST_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER)
+ .alpha(FIRST_BACK_LAYER_ICON_ALPHA),
+ )
+ }
+ TangemIcon(
+ tangemIconUM = tangemIconUM,
+ modifier = baseIconModifier,
+ )
+ }
+}
+
+@Preview(showBackground = true, widthDp = 360)
+@Composable
+private fun LayeringIconsPreview() {
+ val previewCurrencyIcon = TangemIconUM.Currency(
+ currencyIconState = CurrencyIconState.TokenIcon(
+ url = null,
+ topBadgeIconResId = null,
+ fallbackTint = TangemColorPalette.Black,
+ fallbackBackground = TangemColorPalette.Meadow,
+ isGrayscale = false,
+ shouldShowCustomBadge = false,
+ ),
+ )
+ TangemThemePreview {
+ Column(horizontalAlignment = Alignment.End) {
+ LayeringIcons(count = MIN_STACKED_ICON_COUNT, tangemIconUM = previewCurrencyIcon)
+ LayeringIcons(
+ count = MAX_STACKED_ICON_COUNT,
+ tangemIconUM = previewCurrencyIcon,
+ )
+ LayeringIcons(
+ count = MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER,
+ tangemIconUM = previewCurrencyIcon,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt
index 6d7118f812..73ab0434d4 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt
@@ -1,5 +1,6 @@
package com.tangem.features.feed.ui.search
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@@ -8,9 +9,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.*
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@@ -27,16 +27,20 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.ds.button.*
-import com.tangem.core.ui.ds.image.TangemIcon
+import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.feed.ui.search.components.GroupedUserAssetItem
+import com.tangem.features.feed.ui.search.components.SingleUserAssetItem
import com.tangem.features.feed.ui.search.state.*
+import kotlinx.collections.immutable.ImmutableList
private const val PLACEHOLDER_COUNT = 10
private const val LOAD_MORE_THRESHOLD = 5
+private const val USER_ASSETS_LIMIT = 3
@Composable
internal fun SearchContent(
@@ -58,6 +62,14 @@ internal fun SearchContent(
lazyListState.scrollToItem(0)
}
+ var isUserAssetsExpanded by rememberSaveable { mutableStateOf(false) }
+ val shouldShowUserAssetsPortfolio = content is SearchContentUM.Results && content.userAssets.isNotEmpty()
+ LaunchedEffect(shouldShowUserAssetsPortfolio) {
+ if (!shouldShowUserAssetsPortfolio) {
+ isUserAssetsExpanded = false
+ }
+ }
+
LazyColumn(
state = lazyListState,
modifier = modifier
@@ -80,6 +92,8 @@ internal fun SearchContent(
)
is SearchContentUM.Results -> searchResultsItems(
results = content,
+ isUserAssetsExpanded = isUserAssetsExpanded,
+ onUserAssetsExpandedChange = { isUserAssetsExpanded = it },
onResultMarketTokenClick = searchCallbacks.onResultMarketTokenClick,
)
}
@@ -147,18 +161,19 @@ private fun LazyListScope.searchHistoryItems(
private fun LazyListScope.searchResultsItems(
results: SearchContentUM.Results,
+ isUserAssetsExpanded: Boolean,
+ onUserAssetsExpandedChange: (Boolean) -> Unit,
onResultMarketTokenClick: (MarketsListItemUM) -> Unit,
) {
if (results.userAssets.isNotEmpty()) {
item(key = "header_portfolio") {
SectionHeader(title = stringResourceSafe(R.string.markets_search_portfolio_header))
}
- items(
- items = results.userAssets,
- key = { it.id },
- ) { asset ->
- UserAssetItem(asset)
- }
+ userAssetsPortfolioItems(
+ assets = results.userAssets,
+ expanded = isUserAssetsExpanded,
+ onExpandedChange = onUserAssetsExpandedChange,
+ )
}
when (val market = results.marketTokens) {
@@ -175,6 +190,42 @@ private fun LazyListScope.searchResultsItems(
}
}
+private fun LazyListScope.userAssetsPortfolioItems(
+ assets: ImmutableList,
+ expanded: Boolean,
+ onExpandedChange: (Boolean) -> Unit,
+) {
+ val shouldShowToggle = assets.size > USER_ASSETS_LIMIT
+ val visibleCount = if (shouldShowToggle && !expanded) USER_ASSETS_LIMIT else assets.size
+
+ items(
+ count = visibleCount,
+ key = { index -> "user_asset_${assets[index].id}" },
+ ) { index ->
+ Column(
+ modifier = Modifier.animateItem(
+ fadeInSpec = tween(durationMillis = 300),
+ fadeOutSpec = tween(durationMillis = 250),
+ ),
+ ) {
+ UserAssetItem(assets[index])
+ SpacerH(TangemTheme.dimens2.x2)
+ }
+ }
+ if (shouldShowToggle) {
+ item(key = "user_assets_show_toggle") {
+ ShowAllUserAssetsButton(
+ modifier = Modifier.animateItem(
+ fadeInSpec = tween(durationMillis = 300),
+ fadeOutSpec = tween(durationMillis = 250),
+ ),
+ isExpanded = expanded,
+ onClick = { onExpandedChange(!expanded) },
+ )
+ }
+ }
+}
+
private fun LazyListScope.marketSearchResultItems(
market: MarketSearchResultUM.Content,
hasUserAssetsSection: Boolean,
@@ -234,7 +285,7 @@ private fun LazyListScope.marketSearchResultNotFoundItem() {
) {
Text(
text = stringResourceSafe(R.string.common_no_results),
- style = TangemTheme.typography2.bodyRegular14,
+ style = TangemTheme.typography2.subheadlineMedium14,
color = TangemTheme.colors2.text.neutral.tertiary,
)
}
@@ -278,106 +329,34 @@ private fun TextHintItem(hint: TextHintItemUM, onHintClick: () -> Unit) {
@Composable
private fun UserAssetItem(asset: UserAssetItemUM) {
when (asset) {
- is UserAssetItemUM.Single -> SingleUserAssetItem(asset)
- is UserAssetItemUM.Grouped -> GroupedUserAssetItem(asset)
+ is UserAssetItemUM.Single -> SingleUserAssetItem(item = asset)
+ is UserAssetItemUM.Grouped -> GroupedUserAssetItem(item = asset)
}
}
@Composable
-private fun SingleUserAssetItem(asset: UserAssetItemUM.Single) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .clickable(onClick = asset.onClick)
- .padding(horizontal = 12.dp, vertical = 14.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp),
+private fun ShowAllUserAssetsButton(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier.fillMaxWidth(),
+ contentAlignment = Alignment.Center,
) {
- TangemIcon(
- modifier = Modifier.size(40.dp),
- tangemIconUM = asset.icon,
+ TangemButton(
+ buttonUM = TangemButtonUM(
+ text = if (isExpanded) {
+ resourceReference(R.string.feed_search_show_less_user_assets)
+ } else {
+ resourceReference(R.string.feed_search_show_all_user_assets)
+ },
+ tangemIconUM = TangemIconUM.Icon(
+ iconRes = if (isExpanded) R.drawable.ic_chewron_up_20 else R.drawable.ic_chewron_down_20,
+ ),
+ iconPosition = TangemButtonIconPosition.End,
+ onClick = onClick,
+ type = TangemButtonType.Secondary,
+ size = TangemButtonSize.X7,
+ shape = TangemButtonShape.Rounded,
+ ),
)
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = asset.tokenName,
- style = TangemTheme.typography2.bodySemibold16,
- color = TangemTheme.colors2.text.neutral.primary,
- maxLines = 1,
- )
- Text(
- text = asset.tokenSymbol,
- style = TangemTheme.typography2.captionRegular13,
- color = TangemTheme.colors2.text.neutral.tertiary,
- maxLines = 1,
- )
- }
- if (!asset.isBalanceHidden) {
- Column(horizontalAlignment = Alignment.End) {
- Text(
- text = asset.fiatBalance,
- style = TangemTheme.typography2.bodySemibold16,
- color = TangemTheme.colors2.text.neutral.primary,
- maxLines = 1,
- )
- Text(
- text = asset.cryptoBalance,
- style = TangemTheme.typography2.captionRegular13,
- color = TangemTheme.colors2.text.neutral.tertiary,
- maxLines = 1,
- )
- }
- }
- }
-}
-
-// TODO [REDACTED_JIRA] update ui item to Portfolio block item
-@Composable
-private fun GroupedUserAssetItem(asset: UserAssetItemUM.Grouped) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .clickable(onClick = asset.onClick)
- .padding(horizontal = 12.dp, vertical = 14.dp),
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- TangemIcon(
- modifier = Modifier.size(40.dp),
- tangemIconUM = asset.icon,
- )
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = asset.tokenName,
- style = TangemTheme.typography2.bodySemibold16,
- color = TangemTheme.colors2.text.neutral.primary,
- maxLines = 1,
- )
- Text(
- text = "${asset.tokenSymbol} · ${asset.tokensCount}",
- style = TangemTheme.typography2.captionRegular13,
- color = TangemTheme.colors2.text.neutral.tertiary,
- maxLines = 1,
- )
- }
- if (!asset.isBalanceHidden) {
- Column(horizontalAlignment = Alignment.End) {
- Text(
- text = asset.totalFiatBalance,
- style = TangemTheme.typography2.bodySemibold16,
- color = TangemTheme.colors2.text.neutral.primary,
- maxLines = 1,
- )
- Text(
- text = asset.totalCryptoBalance,
- style = TangemTheme.typography2.captionRegular13,
- color = TangemTheme.colors2.text.neutral.tertiary,
- maxLines = 1,
- )
- }
- }
- }
}
}
@@ -397,7 +376,7 @@ private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Mod
) {
Text(
text = stringResourceSafe(R.string.markets_search_see_tokens_under_100k),
- style = TangemTheme.typography2.bodyRegular14,
+ style = TangemTheme.typography2.subheadlineMedium14,
color = TangemTheme.colors2.text.neutral.secondary,
)
TangemButton(
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/BalanceColumn.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/BalanceColumn.kt
new file mode 100644
index 0000000000..552f07fcb3
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/BalanceColumn.kt
@@ -0,0 +1,175 @@
+package com.tangem.features.feed.ui.search.components
+
+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.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.R
+import com.tangem.core.ui.components.RectangleShimmer
+import com.tangem.core.ui.components.SpacerW
+import com.tangem.core.ui.components.flicker
+import com.tangem.core.ui.extensions.resolveAnnotatedReference
+import com.tangem.core.ui.extensions.resolveReference
+import com.tangem.core.ui.extensions.stringResourceSafe
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.feed.ui.search.state.BalanceDisplayState
+import com.tangem.utils.StringsSigns
+
+@Composable
+internal fun BalanceColumn(
+ balanceState: BalanceDisplayState,
+ isBalanceHidden: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ if (isBalanceHidden) {
+ HiddenBalance(modifier)
+ return
+ }
+ when (balanceState) {
+ is BalanceDisplayState.Loading -> LoadingBalanceColumn(modifier)
+ is BalanceDisplayState.Flickering -> FlickeringBalanceColumn(balanceState, modifier)
+ is BalanceDisplayState.Stale -> StaleBalanceColumn(balanceState, modifier)
+ is BalanceDisplayState.Unreachable -> UnreachableBalanceColumn(modifier)
+ is BalanceDisplayState.Loaded -> LoadedBalanceColumn(balanceState, modifier)
+ }
+}
+
+@Composable
+private fun BalanceColumnLayout(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.End,
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ content = content,
+ )
+}
+
+@Composable
+private fun LoadingBalanceColumn(modifier: Modifier = Modifier) {
+ BalanceColumnLayout(modifier) {
+ RectangleShimmer(modifier = Modifier.size(width = 108.dp, height = 20.dp), radius = 20.dp)
+ RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 16.dp), radius = 20.dp)
+ }
+}
+
+@Composable
+private fun FlickeringBalanceColumn(state: BalanceDisplayState.Flickering, modifier: Modifier = Modifier) {
+ val flickerModifier = Modifier.flicker(isFlickering = true)
+ BalanceColumnLayout(modifier) {
+ Text(
+ modifier = flickerModifier,
+ text = state.fiatBalance.resolveAnnotatedReference(),
+ style = TangemTheme.typography2.bodyMedium16,
+ color = TangemTheme.colors2.text.neutral.primary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Text(
+ modifier = flickerModifier,
+ text = state.cryptoBalance.resolveReference(),
+ style = TangemTheme.typography2.captionMedium12,
+ color = TangemTheme.colors2.text.neutral.secondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+}
+
+@Composable
+private fun StaleBalanceColumn(state: BalanceDisplayState.Stale, modifier: Modifier = Modifier) {
+ BalanceColumnLayout(modifier) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(
+ modifier = Modifier.size(16.dp),
+ painter = painterResource(id = R.drawable.ic_error_sync_24),
+ contentDescription = null,
+ tint = TangemTheme.colors2.markers.iconGray,
+ )
+ SpacerW(TangemTheme.dimens2.x0_5)
+ Text(
+ text = state.fiatBalance.resolveAnnotatedReference(),
+ style = TangemTheme.typography2.bodyMedium16,
+ color = TangemTheme.colors2.text.neutral.primary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ Text(
+ text = state.cryptoBalance.resolveReference(),
+ style = TangemTheme.typography2.captionMedium12,
+ color = TangemTheme.colors2.text.neutral.secondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+}
+
+@Composable
+private fun UnreachableBalanceColumn(modifier: Modifier = Modifier) {
+ BalanceColumnLayout(modifier) {
+ Text(
+ text = StringsSigns.DASH_SIGN,
+ style = TangemTheme.typography2.bodyMedium16,
+ color = TangemTheme.colors2.text.neutral.primary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = stringResourceSafe(R.string.common_unreachable),
+ style = TangemTheme.typography2.captionMedium12,
+ color = TangemTheme.colors2.text.status.attention,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ SpacerW(TangemTheme.dimens2.x0_5)
+ Icon(
+ modifier = Modifier.size(16.dp),
+ painter = painterResource(id = R.drawable.ic_alert_triange_24),
+ contentDescription = null,
+ tint = TangemTheme.colors2.graphic.status.attention,
+ )
+ }
+ }
+}
+
+@Composable
+private fun LoadedBalanceColumn(state: BalanceDisplayState.Loaded, modifier: Modifier = Modifier) {
+ BalanceColumnLayout(modifier) {
+ Text(
+ text = state.fiatBalance.resolveAnnotatedReference(),
+ style = TangemTheme.typography2.bodySemibold16,
+ color = TangemTheme.colors2.text.neutral.primary,
+ maxLines = 1,
+ )
+ Text(
+ text = state.cryptoBalance.resolveReference(),
+ style = TangemTheme.typography2.captionMedium12,
+ color = TangemTheme.colors2.text.neutral.secondary,
+ maxLines = 1,
+ )
+ }
+}
+
+@Composable
+private fun HiddenBalance(modifier: Modifier = Modifier) {
+ BalanceColumnLayout(modifier) {
+ Text(
+ text = StringsSigns.THREE_STARS,
+ style = TangemTheme.typography2.bodySemibold16,
+ color = TangemTheme.colors2.text.neutral.primary,
+ maxLines = 1,
+ )
+ Text(
+ text = StringsSigns.THREE_STARS,
+ style = TangemTheme.typography2.captionMedium12,
+ color = TangemTheme.colors2.text.neutral.secondary,
+ maxLines = 1,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/GroupedUserAssetItem.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/GroupedUserAssetItem.kt
new file mode 100644
index 0000000000..11504f43d5
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/GroupedUserAssetItem.kt
@@ -0,0 +1,84 @@
+package com.tangem.features.feed.ui.search.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.text.style.TextOverflow
+import com.tangem.core.ui.ds.button.*
+import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.ds.row.TangemRowContainer
+import com.tangem.core.ui.ds.row.TangemRowLayoutId
+import com.tangem.core.ui.extensions.pluralStringResourceSafe
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.feed.impl.R
+import com.tangem.features.feed.ui.components.LayeringIcons
+import com.tangem.features.feed.ui.search.state.UserAssetItemUM
+
+@Composable
+internal fun GroupedUserAssetItem(item: UserAssetItemUM.Grouped, modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier.background(
+ color = TangemTheme.colors2.surface.level3,
+ shape = RoundedCornerShape(TangemTheme.dimens2.x5),
+ ),
+ ) {
+ TangemRowContainer(
+ modifier = Modifier.clickable(onClick = item.onClick),
+ content = {
+ LayeringIcons(
+ modifier = Modifier
+ .layoutId(layoutId = TangemRowLayoutId.HEAD)
+ .padding(end = TangemTheme.dimens2.x1),
+ tangemIconUM = item.icon,
+ count = item.tokensCount,
+ )
+
+ Text(
+ modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
+ text = item.tokenName,
+ style = TangemTheme.typography2.bodyMedium16,
+ color = TangemTheme.colors2.text.neutral.primary,
+ maxLines = 1,
+ overflow = TextOverflow.MiddleEllipsis,
+ )
+
+ Text(
+ modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
+ text = pluralStringResourceSafe(R.plurals.common_tokens_count, item.tokensCount, item.tokensCount),
+ style = TangemTheme.typography2.captionMedium12,
+ color = TangemTheme.colors2.text.neutral.secondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+
+ BalanceColumn(
+ modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_TOP),
+ balanceState = item.balanceState,
+ isBalanceHidden = item.isBalanceHidden,
+ )
+
+ TangemButton(
+ modifier = Modifier
+ .padding(start = TangemTheme.dimens2.x2)
+ .layoutId(TangemRowLayoutId.TAIL),
+ buttonUM = TangemButtonUM(
+ type = TangemButtonType.Secondary,
+ tangemIconUM = TangemIconUM.Icon(
+ iconRes = R.drawable.ic_chevron_24,
+ tintReference = { TangemTheme.colors2.graphic.neutral.primary },
+ ),
+ shape = TangemButtonShape.Rounded,
+ size = TangemButtonSize.X10,
+ onClick = item.onClick,
+ ),
+ )
+ },
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/SingleUserAssetItem.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/SingleUserAssetItem.kt
new file mode 100644
index 0000000000..cf255f66a5
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/SingleUserAssetItem.kt
@@ -0,0 +1,117 @@
+package com.tangem.features.feed.ui.search.components
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
+import com.tangem.core.ui.components.marketprice.PriceChangeState
+import com.tangem.core.ui.ds.image.TangemIcon
+import com.tangem.core.ui.ds.row.TangemRowContainer
+import com.tangem.core.ui.ds.row.TangemRowLayoutId
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.feed.ui.search.state.BalanceDisplayState
+import com.tangem.features.feed.ui.search.state.UserAssetItemUM
+
+@Composable
+fun SingleUserAssetItem(item: UserAssetItemUM.Single, modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier.background(
+ color = TangemTheme.colors2.surface.level3,
+ shape = RoundedCornerShape(TangemTheme.dimens2.x5),
+ ),
+ ) {
+ TangemRowContainer(
+ modifier = Modifier.clickable(onClick = item.onClick),
+ content = {
+ TangemIcon(
+ modifier = Modifier
+ .layoutId(layoutId = TangemRowLayoutId.HEAD)
+ .size(40.dp)
+ .padding(end = TangemTheme.dimens2.x1),
+ tangemIconUM = item.icon,
+ )
+
+ Text(
+ modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
+ text = item.tokenName,
+ style = TangemTheme.typography2.bodyMedium16,
+ color = TangemTheme.colors2.text.neutral.primary,
+ maxLines = 1,
+ overflow = TextOverflow.MiddleEllipsis,
+ )
+
+ PriceBlock(
+ modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
+ priceChangeState = item.priceChangeState,
+ fiatRate = item.fiatRate,
+ balanceState = item.balanceState,
+ )
+
+ BalanceColumn(
+ modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_TOP),
+ balanceState = item.balanceState,
+ isBalanceHidden = item.isBalanceHidden,
+ )
+ },
+ )
+ }
+}
+
+@Composable
+private fun PriceBlock(
+ priceChangeState: PriceChangeState,
+ fiatRate: String?,
+ balanceState: BalanceDisplayState,
+ modifier: Modifier = Modifier,
+) {
+ val isDisabled = remember(balanceState) {
+ balanceState is BalanceDisplayState.Unreachable
+ }
+
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
+ ) {
+ if (fiatRate != null) {
+ Text(
+ text = fiatRate,
+ style = TangemTheme.typography2.captionMedium12,
+ color = if (isDisabled) {
+ TangemTheme.colors2.text.status.disabled
+ } else {
+ TangemTheme.colors2.text.neutral.secondary
+ },
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+
+ AnimatedContent(
+ targetState = priceChangeState,
+ contentKey = { it::class },
+ ) { animatedState ->
+ when (animatedState) {
+ is PriceChangeState.Content -> {
+ PriceChangeInPercent(
+ valueInPercent = animatedState.valueInPercent,
+ type = animatedState.type,
+ textStyle = TangemTheme.typography2.captionMedium12,
+ isDisabled = isDisabled,
+ )
+ }
+ PriceChangeState.Unknown -> Unit
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt
index e5cd68f79c..58334e7bce 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt
@@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
+import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.stringReference
@@ -217,8 +218,14 @@ internal object SearchContentPreviewFixtures {
tokenName = name,
tokenSymbol = symbol,
fiatRate = "$98,765.43",
- cryptoBalance = "1.234 $symbol",
- fiatBalance = "$121,876.50",
+ priceChangeState = PriceChangeState.Content(
+ type = PriceChangeType.UP,
+ valueInPercent = "+2.34%",
+ ),
+ balanceState = BalanceDisplayState.Loaded(
+ cryptoBalance = stringReference("1.234 $symbol"),
+ fiatBalance = stringReference("$121,876.50"),
+ ),
isBalanceHidden = false,
onClick = {},
)
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/UserAssetItemPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/UserAssetItemPreview.kt
new file mode 100644
index 0000000000..f2f8302d45
--- /dev/null
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/UserAssetItemPreview.kt
@@ -0,0 +1,238 @@
+package com.tangem.features.feed.ui.search.preview
+
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.components.currency.icon.CurrencyIconState
+import com.tangem.core.ui.components.marketprice.PriceChangeState
+import com.tangem.core.ui.components.marketprice.PriceChangeType
+import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreviewRedesign
+import com.tangem.features.feed.ui.search.components.GroupedUserAssetItem
+import com.tangem.features.feed.ui.search.components.SingleUserAssetItem
+import com.tangem.features.feed.ui.search.state.BalanceDisplayState
+import com.tangem.features.feed.ui.search.state.UserAssetItemUM
+import kotlinx.collections.immutable.persistentListOf
+
+/** Labeled UI state for [SingleUserAssetItem] previews (dropdown label in Studio). */
+internal data class SingleUserAssetItemPreviewScenario(
+ val title: String,
+ val item: UserAssetItemUM.Single,
+)
+
+/** Labeled UI state for [GroupedUserAssetItem] previews. */
+internal data class GroupedUserAssetItemPreviewScenario(
+ val title: String,
+ val item: UserAssetItemUM.Grouped,
+)
+
+@Suppress("StringLiteralDuplication")
+internal object UserAssetItemPreviewFixtures {
+
+ private val sampleIcon = TangemIconUM.Currency(
+ CurrencyIconState.CoinIcon(
+ url = null,
+ fallbackResId = com.tangem.core.ui.R.drawable.ic_ethereumpow_22,
+ isGrayscale = false,
+ shouldShowCustomBadge = false,
+ ),
+ )
+
+ private val cryptoRef = stringReference("1.234 ETH")
+ private val fiatRef = stringReference("$121,876.50")
+
+ private val samplePriceChange = PriceChangeState.Content(
+ valueInPercent = "+2.34%",
+ type = PriceChangeType.UP,
+ )
+
+ private fun balanceLoaded() = BalanceDisplayState.Loaded(
+ cryptoBalance = cryptoRef,
+ fiatBalance = fiatRef,
+ )
+
+ private fun balanceFlickering() = BalanceDisplayState.Flickering(
+ cryptoBalance = cryptoRef,
+ fiatBalance = fiatRef,
+ )
+
+ private fun balanceStale() = BalanceDisplayState.Stale(
+ cryptoBalance = cryptoRef,
+ fiatBalance = fiatRef,
+ )
+
+ private fun balanceLoading() = BalanceDisplayState.Loading
+
+ private fun balanceUnreachable() = BalanceDisplayState.Unreachable
+
+ fun allSingleScenarios(): List = listOf(
+ SingleUserAssetItemPreviewScenario(
+ title = "Hidden (balanceState ignored)",
+ item = single(
+ balanceState = balanceLoaded(),
+ isBalanceHidden = true,
+ ),
+ ),
+ SingleUserAssetItemPreviewScenario(
+ title = "Balance – Loaded",
+ item = single(balanceState = balanceLoaded(), isBalanceHidden = false),
+ ),
+ SingleUserAssetItemPreviewScenario(
+ title = "Balance – Flickering",
+ item = single(balanceState = balanceFlickering(), isBalanceHidden = false),
+ ),
+ SingleUserAssetItemPreviewScenario(
+ title = "Balance – Stale",
+ item = single(balanceState = balanceStale(), isBalanceHidden = false),
+ ),
+ SingleUserAssetItemPreviewScenario(
+ title = "Balance – Loading",
+ item = single(balanceState = balanceLoading(), isBalanceHidden = false),
+ ),
+ SingleUserAssetItemPreviewScenario(
+ title = "Balance – Unreachable",
+ item = single(balanceState = balanceUnreachable(), isBalanceHidden = false),
+ ),
+ )
+
+ fun allGroupedScenarios(): List = listOf(
+ GroupedUserAssetItemPreviewScenario(
+ title = "Hidden (balanceState ignored)",
+ item = grouped(
+ balanceState = balanceLoaded(),
+ isBalanceHidden = true,
+ ),
+ ),
+ GroupedUserAssetItemPreviewScenario(
+ title = "Balance – Loaded",
+ item = grouped(balanceState = balanceLoaded(), isBalanceHidden = false),
+ ),
+ GroupedUserAssetItemPreviewScenario(
+ title = "Balance – Flickering",
+ item = grouped(balanceState = balanceFlickering(), isBalanceHidden = false),
+ ),
+ GroupedUserAssetItemPreviewScenario(
+ title = "Balance – Stale",
+ item = grouped(balanceState = balanceStale(), isBalanceHidden = false),
+ ),
+ GroupedUserAssetItemPreviewScenario(
+ title = "Balance – Loading",
+ item = grouped(balanceState = balanceLoading(), isBalanceHidden = false),
+ ),
+ GroupedUserAssetItemPreviewScenario(
+ title = "Balance – Unreachable",
+ item = grouped(balanceState = balanceUnreachable(), isBalanceHidden = false),
+ ),
+ )
+
+ private fun single(balanceState: BalanceDisplayState, isBalanceHidden: Boolean): UserAssetItemUM.Single =
+ UserAssetItemUM.Single(
+ id = "single_preview",
+ icon = sampleIcon,
+ tokenName = "Ethereum",
+ tokenSymbol = "ETH",
+ fiatRate = "$98,765.43",
+ priceChangeState = samplePriceChange,
+ balanceState = balanceState,
+ isBalanceHidden = isBalanceHidden,
+ onClick = {},
+ )
+
+ private fun grouped(balanceState: BalanceDisplayState, isBalanceHidden: Boolean): UserAssetItemUM.Grouped =
+ UserAssetItemUM.Grouped(
+ id = "grouped_preview",
+ icon = sampleIcon,
+ tokenName = "Ethereum",
+ tokenSymbol = "ETH",
+ tokensCount = 3,
+ balanceState = balanceState,
+ isBalanceHidden = isBalanceHidden,
+ children = persistentListOf(),
+ onClick = {},
+ )
+}
+
+internal class SingleUserAssetItemPreviewParameterProvider :
+ PreviewParameterProvider {
+ override val values: Sequence
+ get() = UserAssetItemPreviewFixtures.allSingleScenarios().asSequence()
+}
+
+internal class GroupedUserAssetItemPreviewParameterProvider :
+ PreviewParameterProvider {
+ override val values: Sequence
+ get() = UserAssetItemPreviewFixtures.allGroupedScenarios().asSequence()
+}
+
+@Composable
+private fun SingleUserAssetItemPreviewHost(
+ scenario: SingleUserAssetItemPreviewScenario,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(TangemTheme.colors2.surface.level1)
+ .padding(8.dp),
+ ) {
+ SingleUserAssetItem(item = scenario.item)
+ }
+}
+
+@Composable
+private fun GroupedUserAssetItemPreviewHost(
+ scenario: GroupedUserAssetItemPreviewScenario,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(TangemTheme.colors2.surface.level1)
+ .padding(8.dp),
+ ) {
+ GroupedUserAssetItem(item = scenario.item)
+ }
+}
+
+@Composable
+@Preview(name = "Single – all balance states (parameter)", showBackground = true, widthDp = 360)
+@Preview(
+ name = "Single – all balance states (night)",
+ showBackground = true,
+ widthDp = 360,
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+)
+private fun SingleUserAssetItemPreview_AllBalanceStates(
+ @PreviewParameter(SingleUserAssetItemPreviewParameterProvider::class) scenario: SingleUserAssetItemPreviewScenario,
+) {
+ TangemThemePreviewRedesign {
+ SingleUserAssetItemPreviewHost(scenario = scenario)
+ }
+}
+
+@Composable
+@Preview(name = "Grouped – all balance states (parameter)", showBackground = true, widthDp = 360)
+@Preview(
+ name = "Grouped – all balance states (night)",
+ showBackground = true,
+ widthDp = 360,
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+)
+private fun GroupedUserAssetItemPreview_AllBalanceStates(
+ @PreviewParameter(GroupedUserAssetItemPreviewParameterProvider::class)
+ scenario: GroupedUserAssetItemPreviewScenario,
+) {
+ TangemThemePreviewRedesign {
+ GroupedUserAssetItemPreviewHost(scenario = scenario)
+ }
+}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt
index ede3decb1b..31277562c6 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt
@@ -4,8 +4,11 @@ import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.markets.models.MarketsListItemUM
import com.tangem.core.ui.components.fields.entity.SearchBarUM
+import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
+import com.tangem.domain.models.currency.CryptoCurrencyStatus
import kotlinx.collections.immutable.ImmutableList
data class SearchUM(
@@ -45,6 +48,28 @@ sealed interface MarketSearchResultUM {
data class TextHintItemUM(val text: String)
+@Immutable
+sealed interface BalanceDisplayState {
+
+ data class Loaded(
+ val cryptoBalance: TextReference,
+ val fiatBalance: TextReference,
+ ) : BalanceDisplayState
+
+ data class Flickering(
+ val cryptoBalance: TextReference,
+ val fiatBalance: TextReference,
+ ) : BalanceDisplayState
+
+ data class Stale(
+ val cryptoBalance: TextReference,
+ val fiatBalance: TextReference,
+ ) : BalanceDisplayState
+
+ data object Loading : BalanceDisplayState
+ data object Unreachable : BalanceDisplayState
+}
+
@Immutable
sealed interface UserAssetItemUM {
val id: String
@@ -59,8 +84,8 @@ sealed interface UserAssetItemUM {
override val tokenName: String,
override val tokenSymbol: String,
val fiatRate: String?,
- val cryptoBalance: String,
- val fiatBalance: String,
+ val priceChangeState: PriceChangeState,
+ val balanceState: BalanceDisplayState,
val isBalanceHidden: Boolean,
override val onClick: () -> Unit,
) : UserAssetItemUM
@@ -71,8 +96,7 @@ sealed interface UserAssetItemUM {
override val tokenName: String,
override val tokenSymbol: String,
val tokensCount: Int,
- val totalCryptoBalance: String,
- val totalFiatBalance: String,
+ val balanceState: BalanceDisplayState,
val isBalanceHidden: Boolean,
val children: ImmutableList,
override val onClick: () -> Unit,
@@ -83,7 +107,6 @@ sealed interface UserAssetItemUM {
val accountName: AccountNameUM,
val accountIcon: CryptoPortfolioIcon.Icon,
val accountColor: CryptoPortfolioIcon.Color,
- val cryptoBalance: String,
- val fiatBalance: String,
+ val currencyStatus: CryptoCurrencyStatus,
)
}
\ No newline at end of file
diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt
index 2bbfa496e2..5963f2ea34 100644
--- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt
+++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt
@@ -27,11 +27,7 @@ internal fun contentFeedEntryStackAnimation(): StackAnimation<
ComposableModularBottomSheetContentComponent,
> =
stackAnimation { to, from, _ ->
- val isSearchToTokenList =
- (to.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
- val isFromSearchTokenList =
- (from.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
- if (isSearchToTokenList || isFromSearchTokenList) {
+ if (to.configuration.usesFadeStackTransition() || from.configuration.usesFadeStackTransition()) {
fade()
} else {
slide()
diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt
index 820fa36229..79c058b390 100644
--- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt
+++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt
@@ -106,6 +106,7 @@ private fun LeftSide(
modifier = Modifier.alignByBaseline(),
valueInPercent = percentText,
type = type,
+ textStyle = TangemTheme.typography.body2,
)
Text(
modifier = Modifier.alignByBaseline(),