Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-17 19:54:07 +07:00
parent 380e7ef082
commit d019d88149
10 changed files with 342 additions and 32 deletions

View file

@ -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,
)
}
}

View file

@ -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<PortfolioTokensListItemUM>,
) : 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
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19.19,11.345H14.32C13.239,11.345 12.606,10.713 12.606,9.632V4.868C12.606,4.261 13.072,3.813 13.688,3.813C14.294,3.813 14.751,4.253 14.751,4.868V5.413L14.558,7.944L16.421,5.984L18.82,3.567C19.022,3.356 19.286,3.259 19.576,3.259C20.235,3.259 20.701,3.708 20.701,4.367C20.701,4.666 20.587,4.929 20.376,5.131L17.968,7.54L16.008,9.394L18.557,9.201H19.19C19.805,9.201 20.253,9.649 20.253,10.273C20.253,10.888 19.805,11.345 19.19,11.345ZM10.356,20.213C9.741,20.213 9.284,19.774 9.284,19.159V18.544L9.478,16.012L7.623,17.972L5.18,20.433C4.978,20.635 4.714,20.741 4.424,20.741C3.765,20.741 3.299,20.284 3.299,19.625C3.299,19.335 3.413,19.071 3.624,18.869L6.076,16.417L8.036,14.562L5.479,14.755H4.784C4.169,14.755 3.721,14.307 3.721,13.683C3.721,13.068 4.169,12.611 4.784,12.611H9.715C10.805,12.611 11.429,13.244 11.429,14.325V19.159C11.429,19.765 10.972,20.213 10.356,20.213Z"
android:fillColor="#1E1E1E"/>
</vector>

View file

@ -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<PortfolioTokensListItemUM>(),
isExpanded = false,
state = AccountItemPreviewData.accountItem
.copy(iconState = AccountItemPreviewData.accountLetterIcon),
),
TokensListItemUM.Portfolio(
tokens = textContentTokensState.items.filterIsInstance<PortfolioTokensListItemUM>(),
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),
),
)
}

View file

@ -29,8 +29,13 @@ internal sealed class WalletTokensListState {
override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?,
) : ContentState()
data class PortfolioContent(
override val items: ImmutableList<TokensListItemUM.Portfolio>,
override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?,
) : ContentState()
data object Locked : ContentState() {
override val items = persistentListOf(
override val items: ImmutableList<TokensListItemUM> = persistentListOf(
TokensListItemUM.GroupTitle(
id = 42,
text = resourceReference(

View file

@ -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<WalletScree
get() = sequenceOf(
walletScreenState,
walletScreenState.copy(selectedWalletIndex = 1),
accountScreenState.copy(selectedWalletIndex = 1),
)
}
// endregion

View file

@ -0,0 +1,160 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
import androidx.compose.animation.*
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.semantics
import com.tangem.core.ui.components.tokenlist.PortfolioListItem
import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.core.ui.utils.lazyListItemPosition
import kotlinx.collections.immutable.ImmutableList
internal fun LazyListScope.portfolioContentItems(
items: ImmutableList<TokensListItemUM.Portfolio>,
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<Boolean>,
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 }

View file

@ -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)
}
}

View file

@ -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,
)
}

View file

@ -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,