diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index 34bd4345fa..ff3b869c1f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -1,13 +1,29 @@ package com.tangem.core.ui.components.tokenlist +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.SpacerW4 +import com.tangem.core.ui.components.account.AccountCharIcon +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.components.account.AccountResIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem +import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -24,16 +40,9 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { when (state) { - is TokensListItemUM.GroupTitle -> { - GroupTitleItem(state, modifier) - } - is TokensListItemUM.Token -> { - TokenItem( - state = state.state, - isBalanceHidden = isBalanceHidden, - modifier = modifier, - ) - } + is TokensListItemUM.GroupTitle -> PortfolioTokensListItem(state, isBalanceHidden, modifier) + is TokensListItemUM.Token -> PortfolioTokensListItem(state, isBalanceHidden, modifier) + is TokensListItemUM.Portfolio -> PortfolioListItem(state, isBalanceHidden, modifier) is TokensListItemUM.SearchBar -> { SearchBar(state = state.searchBarUM, modifier = modifier.padding(all = 12.dp)) } @@ -46,4 +55,86 @@ fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: M ) } } +} + +@Composable +fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + if (state.isExpanded) { + ExpandedPortfolioHeader(state.state, modifier) + } else { + TokenItem( + state = state.state, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + } +} + +@Composable +fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + when (state) { + is TokensListItemUM.GroupTitle -> GroupTitleItem(state, modifier) + is TokensListItemUM.Token -> TokenItem( + state = state.state, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + } +} + +@Composable +private fun ExpandedPortfolioHeader(state: TokenItemState, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickable(onClick = { state.onItemClick?.invoke(state) }) + .padding( + vertical = TangemTheme.dimens.spacing4, + horizontal = TangemTheme.dimens.spacing12, + ), + ) { + when (val icon = state.iconState) { + is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon( + resId = icon.resId, + color = icon.color, + size = AccountIconSize.ExtraSmall, + ) + is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon( + char = icon.char.resolveReference().first(), + color = icon.color, + size = AccountIconSize.ExtraSmall, + ) + is CurrencyIconState.CoinIcon, + is CurrencyIconState.CustomTokenIcon, + is CurrencyIconState.Empty, + is CurrencyIconState.FiatIcon, + CurrencyIconState.Loading, + CurrencyIconState.Locked, + is CurrencyIconState.TokenIcon, + -> Unit + } + + SpacerW4() + + when (val titleState = state.titleState) { + TokenItemState.TitleState.Loading -> Unit + TokenItemState.TitleState.Locked -> Unit + is TokenItemState.TitleState.Content -> Text( + modifier = Modifier.weight(1f), + text = titleState.text.resolveReference(), + color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.caption1, + ) + } + + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_minimize_24), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt index 9b0e9235ed..24da6e9d28 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt @@ -29,16 +29,29 @@ sealed interface TokensListItemUM { * @property id id * @property text title value */ - data class GroupTitle(override val id: Any, val text: TextReference) : TokensListItemUM + data class GroupTitle(override val id: Any, val text: TextReference) : TokensListItemUM, PortfolioTokensListItemUM /** * Token item * * @property state token state */ - data class Token(val state: TokenItemState) : TokensListItemUM { + data class Token(val state: TokenItemState) : TokensListItemUM, PortfolioTokensListItemUM { + override val id: String = state.id + } + + data class Portfolio( + val state: TokenItemState, + val isExpanded: Boolean, + val tokens: List, + ) : TokensListItemUM { override val id: String = state.id } data class Text(override val id: Any, val text: TextReference) : TokensListItemUM +} + +sealed interface PortfolioTokensListItemUM { + /** Unique ID */ + val id: Any } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_minimize_24.xml b/core/ui/src/main/res/drawable/ic_minimize_24.xml new file mode 100644 index 0000000000..b3a32f2864 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_minimize_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 9e485dad2e..8dee141518 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -5,14 +5,15 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState +import com.tangem.core.ui.components.token.AccountItemPreviewData import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig import com.tangem.feature.wallet.presentation.wallet.state.model.* @@ -82,6 +83,26 @@ internal object WalletScreenPreviewData { ), ) + private val portfolioContentState = WalletTokensListState.ContentState.PortfolioContent( + items = persistentListOf( + TokensListItemUM.Portfolio( + tokens = textContentTokensState.items.filterIsInstance(), + isExpanded = false, + state = AccountItemPreviewData.accountItem + .copy(iconState = AccountItemPreviewData.accountLetterIcon), + ), + TokensListItemUM.Portfolio( + tokens = textContentTokensState.items.filterIsInstance(), + isExpanded = true, + state = AccountItemPreviewData.accountItem, + ), + ), + organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( + isEnabled = true, + onClick = {}, + ), + ) + private val noteLockedCard by lazy { WalletCardState.LockedContent( id = UserWalletId(stringValue = "1"), @@ -175,4 +196,12 @@ internal object WalletScreenPreviewData { showMarketsOnboarding = false, onDismissMarketsOnboarding = {}, ) + + internal val accountScreenState = + walletScreenState.copy( + wallets = persistentListOf( + singleWalletLockedState, + multiWalletState.copy(tokensListState = portfolioContentState), + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt index 196ec4d3ba..82050bd5fb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt @@ -29,8 +29,13 @@ internal sealed class WalletTokensListState { override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?, ) : ContentState() + data class PortfolioContent( + override val items: ImmutableList, + override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?, + ) : ContentState() + data object Locked : ContentState() { - override val items = persistentListOf( + override val items: ImmutableList = persistentListOf( TokensListItemUM.GroupTitle( id = 42, text = resourceReference( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 9f053d4423..133702c815 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -40,16 +40,16 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheet +import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheet import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.expressTransactionsItems import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheet -import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.components.rememberIsKeyboardVisible import com.tangem.core.ui.components.sheetscaffold.* @@ -67,18 +67,14 @@ import com.tangem.core.ui.test.MarketTooltipTestTags import com.tangem.core.ui.utils.lineTo import com.tangem.core.ui.utils.moveTo import com.tangem.core.ui.utils.toPx -import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig -import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBottomSheetConfig -import com.tangem.feature.wallet.presentation.wallet.state.model.VisaTxDetailsBottomSheetConfig -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenState import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* -import com.tangem.feature.wallet.presentation.wallet.ui.components.common.actions import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.nftCollections import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock @@ -740,6 +736,7 @@ private class WalletScreenPreviewProvider : PreviewParameterProvider, + modifier: Modifier = Modifier, + isBalanceHidden: Boolean, +) { + items.forEachIndexed { index, item -> + portfolioTokensList( + portfolio = item, + modifier = modifier, + portfolioIndex = index, + isBalanceHidden = isBalanceHidden, + ) + } +} + +internal fun LazyListScope.portfolioTokensList( + portfolio: TokensListItemUM.Portfolio, + modifier: Modifier, + portfolioIndex: Int, + isBalanceHidden: Boolean, +) { + val tokens = portfolio.tokens + val isExpanded = portfolio.isExpanded + + portfolioItem( + portfolio = portfolio, + modifier = modifier, + portfolioIndex = portfolioIndex, + isBalanceHidden = isBalanceHidden, + ) + if (!isExpanded) return + itemsIndexed( + items = tokens, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { tokenIndex, token -> + val indexWithHeader = tokenIndex.inc() + val isPreview = LocalInspectionMode.current + val appear = remember { + MutableTransitionState(isPreview).apply { targetState = true } + } + SlideInItemVisibility( + modifier = modifier + .testModifier(indexWithHeader) + .animateItem() + .roundedShapeItemDecoration( + currentIndex = indexWithHeader, + lastIndex = tokens.lastIndex.inc(), + backgroundColor = TangemTheme.colors.background.primary, + ), + visibleState = appear, + ) { + PortfolioTokensListItem( + state = token, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + } + }, + ) +} + +private fun LazyListScope.portfolioItem( + portfolio: TokensListItemUM.Portfolio, + modifier: Modifier, + portfolioIndex: Int, + isBalanceHidden: Boolean, +) { + val tokens = portfolio.tokens + val isExpanded = portfolio.isExpanded + + item( + key = "account-${portfolio.id}-isExpanded$isExpanded", + contentType = "account-isExpanded$isExpanded", + ) { + val anchorModifier = modifier + .testModifier(portfolioIndex) + .animateItem() + .roundedShapeItemDecoration( + currentIndex = 0, + lastIndex = if (isExpanded) tokens.lastIndex.inc() else 0, + backgroundColor = TangemTheme.colors.background.primary, + ) + val isPreview = LocalInspectionMode.current + val appear = remember { + MutableTransitionState(isPreview).apply { targetState = true } + } + if (isExpanded) { + SlideInItemVisibility( + modifier = anchorModifier, + visibleState = appear, + ) { + PortfolioListItem( + state = portfolio, + isBalanceHidden = isBalanceHidden, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) + } + } else { + AnimatedVisibility( + modifier = anchorModifier, + visibleState = appear, + enter = fadeIn(), + exit = ExitTransition.None, + ) { + PortfolioListItem( + state = portfolio, + isBalanceHidden = isBalanceHidden, + ) + } + } + } +} + +@Composable +private fun SlideInItemVisibility( + visibleState: MutableTransitionState, + modifier: Modifier = Modifier, + content: @Composable() AnimatedVisibilityScope.() -> Unit, +) { + AnimatedVisibility( + modifier = modifier, + visibleState = visibleState, + enter = slideInVertically( + animationSpec = tween(easing = LinearOutSlowInEasing), + initialOffsetY = { it }, + ) + fadeIn(), + exit = ExitTransition.None, + ) { + content() + } +} + +private fun Modifier.testModifier(index: Int): Modifier = this + .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = index } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index d10cd0f107..e8362d171b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -41,13 +41,19 @@ internal fun LazyListScope.tokensListItems( isBalanceHidden: Boolean, ) { when (state) { - is WalletTokensListState.ContentState -> { - contentItems( - items = state.items, - isBalanceHidden = isBalanceHidden, - modifier = modifier, - ) - } + is WalletTokensListState.ContentState.PortfolioContent -> portfolioContentItems( + items = state.items, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + is WalletTokensListState.ContentState.Content, + is WalletTokensListState.ContentState.Loading, + is WalletTokensListState.ContentState.Locked, + -> contentItems( + items = state.items, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) WalletTokensListState.Empty -> nonContentItem(modifier = modifier) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt index ba720b16af..e6257855e9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt @@ -10,7 +10,7 @@ private const val NFT_COLLECTIONS_CONTENT_TYPE = "NFTCollections" internal fun LazyListScope.nftCollections(state: WalletNFTItemUM, modifier: Modifier = Modifier) { item(key = NFT_COLLECTIONS_CONTENT_TYPE, contentType = NFT_COLLECTIONS_CONTENT_TYPE) { WalletNFTItem( - modifier = modifier, + modifier = modifier.animateItem(), state = state, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt index 8597700ca0..d207cb203c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt @@ -26,7 +26,7 @@ internal fun LazyListScope.organizeTokensButton( ) { item(key = ORGANIZE_BUTTON_CONTENT_TYPE, contentType = ORGANIZE_BUTTON_CONTENT_TYPE) { RoundedActionButton( - modifier = modifier.testTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON), + modifier = modifier.animateItem().testTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON), config = ActionButtonConfig( text = resourceReference(id = R.string.organize_tokens_title), iconResId = R.drawable.ic_filter_24,