diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index cee76edbb1..ef3b9c7ac3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.converters import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.domain.NetworkInfo import com.tangem.feature.swap.domain.models.ui.FoundTokensState @@ -8,7 +9,8 @@ import com.tangem.feature.swap.domain.models.ui.TokenWithBalance import com.tangem.feature.swap.models.Network import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData -import com.tangem.feature.swap.models.TokenToSelect +import com.tangem.feature.swap.models.TokenToSelectState +import kotlinx.collections.immutable.toImmutableList class TokensDataConverter( private val onSearchEntered: (String) -> Unit, @@ -18,21 +20,27 @@ class TokensDataConverter( fun convertWithNetwork(value: FoundTokensState, network: NetworkInfo): SwapSelectTokenStateHolder { return SwapSelectTokenStateHolder( - addedTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) }, - otherTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) }, + availableTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), + unavailableTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, network = Network(network.name, network.blockchainId), ) } - private fun tokenWithBalanceToTokenToSelect(tokenWithBalance: TokenWithBalance): TokenToSelect { - return TokenToSelect( + private fun tokenWithBalanceToTokenToSelect(tokenWithBalance: TokenWithBalance): TokenToSelectState.TokenToSelect { + return TokenToSelectState.TokenToSelect( id = tokenWithBalance.token.id, name = tokenWithBalance.token.name, symbol = tokenWithBalance.token.symbol, - iconUrl = tokenWithBalance.token.logoUrl, isNative = tokenWithBalance.token is Currency.NativeToken, + // todo replace converting + tokenIcon = TokenIconState.CoinIcon( + url = "", + fallbackResId = 0, + isGrayscale = false, + showCustomBadge = false, + ), addedTokenBalanceData = TokenBalanceData( amount = tokenWithBalance.tokenBalanceData?.amount, amountEquivalent = tokenWithBalance.tokenBalanceData?.amountEquivalent, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 2e48e108cc..382da8e588 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -1,28 +1,36 @@ package com.tangem.feature.swap.models +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import kotlinx.collections.immutable.ImmutableList + data class SwapSelectTokenStateHolder( - val addedTokens: List, - val otherTokens: List, + val availableTokens: ImmutableList, + val unavailableTokens: ImmutableList, val network: Network, val onSearchEntered: (String) -> Unit, val onTokenSelected: (String) -> Unit, ) -data class TokenToSelect( - val id: String, - val name: String, - val symbol: String, - val iconUrl: String, - val isNative: Boolean, - val available: Boolean = true, - val addedTokenBalanceData: TokenBalanceData? = null, -) - data class Network( val name: String, val blockchainId: String, ) +sealed class TokenToSelectState { + + data class Title(val title: String) : TokenToSelectState() + + data class TokenToSelect( + val id: String, + val name: String, + val symbol: String, + val isNative: Boolean, + val tokenIcon: TokenIconState, + val available: Boolean = true, + val addedTokenBalanceData: TokenBalanceData? = null, + ) : TokenToSelectState() +} + data class TokenBalanceData( val amount: String?, val amountEquivalent: String?, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 75b0e2b05f..af9fc07c61 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -274,11 +274,18 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: isBalanceHidden = isBalanceHidden, ) val selectTokenState = uiState.selectTokenState?.copy( - addedTokens = uiState.selectTokenState.addedTokens.map { - it.copy( - addedTokenBalanceData = it.addedTokenBalanceData?.copy(isBalanceHidden = isBalanceHidden), - ) - }, + availableTokens = uiState.selectTokenState.availableTokens.map { + when (it) { + is TokenToSelectState.TokenToSelect -> { + it.copy( + addedTokenBalanceData = it.addedTokenBalanceData?.copy(isBalanceHidden = isBalanceHidden), + ) + } + is TokenToSelectState.Title -> { + it + } + } + }.toImmutableList(), ) return uiState.copy( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 566447924f..1faa262bb3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -1,45 +1,31 @@ package com.tangem.feature.swap.ui -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.Divider import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest import com.tangem.common.Strings -import com.tangem.core.ui.components.CircleShimmer -import com.tangem.core.ui.components.CurrencyPlaceholderIcon -import com.tangem.core.ui.components.SpacerW2 +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.ExpandableSearchView +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.feature.swap.models.Network -import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.TokenBalanceData -import com.tangem.feature.swap.models.TokenToSelect +import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.R -import kotlinx.coroutines.launch +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList @Composable fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) { @@ -64,7 +50,6 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) ) } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) { val screenBackgroundColor = TangemTheme.colors.background.secondary @@ -74,44 +59,45 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { - if (state.addedTokens.isNotEmpty()) { - stickyHeader { Header(title = R.string.swapping_token_list_your_tokens) } - } + item { SpacerH8() } - itemsIndexed(items = state.addedTokens) { index, item -> - TokenItem( - token = item, - network = state.network, - screenBackgroundColor = screenBackgroundColor, - onTokenClick = { - state - .onTokenSelected(item.id) - }, - ) + tokensToSelectItems(state.availableTokens, state.onTokenSelected) - if (index != state.addedTokens.lastIndex) { - Divider( - color = TangemTheme.colors.stroke.primary, - startIndent = TangemTheme.dimens.spacing54, + item { SpacerH12() } + + tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) + + item { SpacerH12() } + } +} + +private fun LazyListScope.tokensToSelectItems( + items: ImmutableList, + onTokenClick: (String) -> Unit, +) { + itemsIndexed(items = items) { index, item -> + when (item) { + is TokenToSelectState.Title -> { + TitleHeader( + item = item, + modifier = Modifier.roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + ), ) } - } - - if (state.otherTokens.isNotEmpty()) { - stickyHeader { Header(title = R.string.swapping_token_list_other_tokens) } - } - - itemsIndexed(items = state.otherTokens) { index, item -> - TokenItem( - token = item, - network = state.network, - screenBackgroundColor = screenBackgroundColor, - onTokenClick = { state.onTokenSelected(item.id) }, - ) - if (index != state.otherTokens.lastIndex) { - Divider( - color = TangemTheme.colors.stroke.primary, - startIndent = TangemTheme.dimens.spacing54, + is TokenToSelectState.TokenToSelect -> { + TokenItem( + token = item, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + ) + .background(TangemTheme.colors.background.action), + onTokenClick = { + onTokenClick(item.id) + }, ) } } @@ -119,27 +105,35 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = } @Composable -private fun Header(@StringRes title: Int) { - Text( - text = stringResource(id = title).uppercase(), - style = TangemTheme.typography.overline, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier +private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Modifier) { + Box( + modifier = modifier .fillMaxWidth() - .padding( - vertical = TangemTheme.dimens.spacing6, - horizontal = TangemTheme.dimens.spacing16, - ), - textAlign = TextAlign.Start, - ) + .background(TangemTheme.colors.background.action), + ) { + Text( + text = item.title, + style = TangemTheme.typography.overline, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing16, + ), + ) + } } @Suppress("LongMethod") @Composable -private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundColor: Color, onTokenClick: () -> Unit) { +private fun TokenItem( + token: TokenToSelectState.TokenToSelect, + onTokenClick: () -> Unit, + modifier: Modifier = Modifier, +) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() + .height(TangemTheme.dimens.size72) .clickable(onClick = onTokenClick) .padding( vertical = TangemTheme.dimens.spacing14, @@ -148,12 +142,15 @@ private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundCo verticalAlignment = Alignment.CenterVertically, ) { TokenIcon( - token = token, - screenBackgroundColor = screenBackgroundColor, - iconPlaceholder = if (token.isNative) getActiveIconRes(network.blockchainId) else null, + state = token.tokenIcon, + shouldDisplayNetwork = true, ) - Column(modifier = Modifier.align(Alignment.CenterVertically)) { + Column( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = TangemTheme.dimens.spacing12), + ) { Text( text = token.name, style = TangemTheme.typography.subtitle1, @@ -213,65 +210,16 @@ private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundCo } } -@Suppress("MagicNumber") -@Composable -private fun TokenIcon(token: TokenToSelect, screenBackgroundColor: Color, @DrawableRes iconPlaceholder: Int?) { - var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } - val isDarkTheme = isSystemInDarkTheme() - val coroutineScope = rememberCoroutineScope() - - val data = token.iconUrl.ifEmpty { - iconPlaceholder - } - Box( - modifier = Modifier - .padding(end = TangemTheme.dimens.spacing12) - .background( - color = iconBackgroundColor, - shape = TangemTheme.shapes.roundedCorners8, - ), - ) { - val iconModifier = Modifier - .size(TangemTheme.dimens.size40) - val colorFilter = if (!token.available) { - val matrix = ColorMatrix().apply { setToSaturation(0f) } - ColorFilter.colorMatrix(matrix) - } else { - null - } - SubcomposeAsyncImage( - modifier = iconModifier, - model = ImageRequest.Builder(LocalContext.current) - .data(data) - .crossfade(true) - .allowHardware(false) - .listener( - onSuccess = { _, result -> - if (isDarkTheme) { - coroutineScope.launch { - val color = ImageBackgroundContrastChecker( - drawable = result.drawable, - backgroundColor = screenBackgroundColor.toArgb(), - ).getContrastColorIfNeeded(isDarkTheme) - iconBackgroundColor = color - } - } - }, - ).build(), - contentDescription = token.id, - loading = { CircleShimmer(modifier = iconModifier) }, - error = { CurrencyPlaceholderIcon(modifier = iconModifier, id = token.id) }, - alpha = if (!token.available) 0.7f else 1f, - colorFilter = colorFilter, - ) - } -} - -private val token = TokenToSelect( +private val token = TokenToSelectState.TokenToSelect( + tokenIcon = TokenIconState.CoinIcon( + url = "", + fallbackResId = 0, + isGrayscale = false, + showCustomBadge = false, + ), id = "", name = "USDC", symbol = "USDC", - iconUrl = "", isNative = false, addedTokenBalanceData = TokenBalanceData( amount = "15 000 $", @@ -281,17 +229,23 @@ private val token = TokenToSelect( ), ) +private val title = TokenToSelectState.Title( + title = "MY TOKENS", +) + @Preview @Composable private fun TokenScreenPreview() { - SwapSelectTokenScreen( - state = SwapSelectTokenStateHolder( - addedTokens = listOf(token, token, token), - otherTokens = listOf(token, token, token), - onSearchEntered = {}, - onTokenSelected = {}, - network = Network("Ethereum", "ETH"), - ), - onBack = {}, - ) + TangemTheme(isDark = false) { + SwapSelectTokenScreen( + state = SwapSelectTokenStateHolder( + availableTokens = listOf(title, token, token, token).toImmutableList(), + unavailableTokens = listOf(title, token, token, token).toImmutableList(), + onSearchEntered = {}, + onTokenSelected = {}, + network = Network("Ethereum", "ETH"), + ), + onBack = {}, + ) + } } \ No newline at end of file