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(),