Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-09 17:06:22 +03:00
commit 115645bccf
50 changed files with 950 additions and 511 deletions

View file

@ -53,7 +53,7 @@ internal class TokenDetailsSkeletonStateConverter(
pullToRefreshConfig = createPullToRefresh(),
bottomSheetConfig = null,
isBalanceHidden = true,
isCustomToken = value.isCustom,
isCustomToken = value is CryptoCurrency.Token && value.isCustom,
)
}

View file

@ -8,6 +8,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -23,6 +24,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.t
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
internal class TokenDetailsStateFactory(
private val currentStateProvider: Provider<TokenDetailsState>,
@ -89,6 +91,16 @@ internal class TokenDetailsStateFactory(
return tokenDetailsButtonsConverter.convert(actions)
}
fun getLoadingTxHistoryState(): TokenDetailsState {
return currentStateProvider().copy(
txHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),
),
),
)
}
fun getLoadingTxHistoryState(itemsCountEither: Either<TxHistoryStateError, Int>): TokenDetailsState {
return loadingTransactionsStateConverter.convert(value = itemsCountEither)
}

View file

@ -9,6 +9,7 @@ import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
internal class TokenDetailsLoadingTxHistoryConverter(
@ -34,22 +35,28 @@ internal class TokenDetailsLoadingTxHistoryConverter(
private fun convert(value: Int): TokenDetailsState {
val state = currentStateProvider()
val txHistoryContent = state.txHistoryState as TxHistoryState.Content
txHistoryContent.contentItems.update {
PagingData.from(
data = listOf(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) +
MutableList(
size = value,
init = {
TxHistoryState.TxHistoryItemState.Transaction(
state = TransactionState.Loading(it.toString()),
)
},
),
return if (state.txHistoryState is TxHistoryState.Content) {
state.txHistoryState.contentItems.update {
PagingData.from(data = createLoadingItems(value))
}
state
} else {
val txHistoryContent = TxHistoryState.Content(
contentItems = MutableStateFlow(
value = PagingData.from(data = createLoadingItems(value)),
),
)
state.copy(txHistoryState = txHistoryContent)
}
}
return state
private fun createLoadingItems(size: Int): List<TxHistoryState.TxHistoryItemState> {
return buildList {
add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick))
(1..size).forEach {
add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString())))
}
}
}
}

View file

@ -15,10 +15,7 @@ import com.tangem.utils.extensions.isToday
import com.tangem.utils.extensions.isYesterday
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.*
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
@ -37,8 +34,12 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
}
override fun convert(value: Flow<PagingData<TxHistoryItem>>): TxHistoryState {
val txHistoryContent = currentStateProvider().txHistoryState as TxHistoryState.Content
val state = currentStateProvider()
val txHistoryContent = if (state.txHistoryState is TxHistoryState.Content) {
state.txHistoryState
} else {
TxHistoryState.Content(contentItems = MutableStateFlow(PagingData.empty()))
}
// FIXME: TxHistoryRepository should send loading transactions
// [REDACTED_JIRA]
value

View file

@ -9,6 +9,7 @@ import arrow.core.getOrElse
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.Provider
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
@ -35,6 +36,8 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
@ -103,7 +106,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private fun updateContent(selectedWallet: UserWallet) {
updateMarketPrice(selectedWallet = selectedWallet)
updateTxHistory()
updateTxHistory(refresh = false, showItemsLoading = true)
updateWarnings(selectedWallet = selectedWallet)
}
@ -137,6 +140,7 @@ internal class TokenDetailsViewModel @Inject constructor(
getCurrencyWarningsUseCase.invoke(
userWalletId = selectedWallet.walletId,
currency = cryptoCurrency,
derivationPath = cryptoCurrency.network.derivationPath,
)
.distinctUntilChanged()
.onEach { uiState = stateFactory.getStateWithNotifications(it) }
@ -148,6 +152,7 @@ internal class TokenDetailsViewModel @Inject constructor(
getCurrencyStatusUpdatesUseCase(
userWalletId = selectedWallet.walletId,
currencyId = cryptoCurrency.id,
derivationPath = cryptoCurrency.network.derivationPath,
)
.distinctUntilChanged()
.onEach { either ->
@ -162,13 +167,19 @@ internal class TokenDetailsViewModel @Inject constructor(
.saveIn(marketPriceJobHolder)
}
private fun updateTxHistory(refresh: Boolean = false) {
/**
* @param refresh - invalidate cache and get data from remote
* @param showItemsLoading - show loading items placeholder.
*/
@Suppress("UnusedPrivateMember") // will be removed after implement caching
private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean) {
viewModelScope.launch(dispatchers.io) {
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
network = cryptoCurrency.network,
)
if (!refresh) {
// if countEither is left, handling error state run inside getLoadingTxHistoryState
if (showItemsLoading || txHistoryItemsCountEither.isLeft()) {
uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither)
}
@ -211,7 +222,8 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onReloadClick() {
analyticsEventsHandler.send(TokenScreenEvent.ButtonReload(cryptoCurrency.symbol))
updateTxHistory()
uiState = stateFactory.getLoadingTxHistoryState()
updateTxHistory(refresh = true, showItemsLoading = true)
}
override fun onSendClick() {
@ -237,6 +249,7 @@ internal class TokenDetailsViewModel @Inject constructor(
getNetworkCoinStatusUseCase(
userWalletId = wallet.walletId,
networkId = status.currency.network.id,
derivationPath = status.currency.network.derivationPath,
)
.take(count = 1)
.collectLatest {
@ -358,13 +371,23 @@ internal class TokenDetailsViewModel @Inject constructor(
uiState = stateFactory.getRefreshingState()
viewModelScope.launch(dispatchers.io) {
fetchCurrencyStatusUseCase.invoke(
userWalletId = wallet.walletId,
id = cryptoCurrency.id,
refresh = true,
)
updateTxHistory(refresh = true)
updateWarnings(wallet)
listOf(
async {
fetchCurrencyStatusUseCase.invoke(
userWalletId = wallet.walletId,
id = cryptoCurrency.id,
derivationPath = cryptoCurrency.network.derivationPath,
refresh = true,
)
},
async {
updateTxHistory(
refresh = true,
showItemsLoading = uiState.txHistoryState !is TxHistoryState.Content,
)
},
async { updateWarnings(wallet) },
).awaitAll()
uiState = stateFactory.getRefreshedState()
}.saveIn(refreshStateJobHolder)
}

View file

@ -1,7 +1,6 @@
package com.tangem.feature.wallet.presentation.common
import androidx.paging.PagingData
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.marketprice.PriceChangeState
@ -13,6 +12,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
@ -37,10 +37,10 @@ internal object WalletPreviewData {
val walletCardContentState by lazy {
WalletCardState.Content(
id = UserWalletId(stringValue = "123"),
title = "Wallet 1",
balance = "8923,05 $",
additionalInfo = TextReference.Str("3 cards • Seed phrase"),
imageResId = R.drawable.ill_businessman_3d,
title = "Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1",
balance = "8923,05312312312312312312331231231233432423423424234 $",
additionalInfo = TextReference.Str("3 cards • Seed phrase3 cards • Seed phraseцфвцфвфцвцфввцфвцф"),
imageResId = R.drawable.ill_wallet2_cards3_120_106,
onRenameClick = { _, _ -> },
onDeleteClick = {},
cardCount = 1,
@ -51,7 +51,7 @@ internal object WalletPreviewData {
WalletCardState.Loading(
id = UserWalletId("321"),
title = "Wallet 1",
imageResId = R.drawable.ill_businessman_3d,
imageResId = R.drawable.ill_wallet2_cards3_120_106,
onRenameClick = { _, _ -> },
onDeleteClick = {},
)
@ -61,7 +61,7 @@ internal object WalletPreviewData {
WalletCardState.HiddenContent(
id = UserWalletId("42"),
title = "Wallet 1",
imageResId = R.drawable.ill_businessman_3d,
imageResId = R.drawable.ill_wallet2_cards3_120_106,
onRenameClick = { _, _ -> },
onDeleteClick = {},
balance = "8923,05 $",
@ -74,7 +74,7 @@ internal object WalletPreviewData {
WalletCardState.Error(
id = UserWalletId("24"),
title = "Wallet 1",
imageResId = R.drawable.ill_businessman_3d,
imageResId = R.drawable.ill_wallet2_cards3_120_106,
onRenameClick = { _, _ -> },
onDeleteClick = {},
)
@ -167,6 +167,7 @@ internal object WalletPreviewData {
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "3 172,14 $"),
isBalanceHidden = false,
)
}

View file

@ -3,19 +3,16 @@ package com.tangem.feature.wallet.presentation.common.component
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.constraintlayout.compose.*
import androidx.compose.ui.unit.Constraints
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.res.TangemTheme
@ -24,152 +21,66 @@ import com.tangem.feature.wallet.presentation.common.component.token.*
import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import kotlin.math.max
private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.22
private const val PRICE_CHANGE_MIN_WIDTH_COEFFICIENT = 0.16
private enum class LayoutId {
ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, PRICE_CHANGE, NON_FIAT_CONTENT
}
@Suppress("LongMethod")
@Composable
internal fun TokenItem(
state: TokenItemState,
modifier: Modifier = Modifier,
reorderableTokenListState: ReorderableLazyListState? = null,
) {
var rootWidth by remember { mutableStateOf(Int.MIN_VALUE) }
@Suppress("DestructuringDeclarationWithTooManyEntries")
BaseContainer(
CustomContainer(
state = state,
modifier = modifier
.tokenClickable(state)
.onSizeChanged { rootWidth = it.width },
.tokenClickable(state = state)
.background(color = TangemTheme.colors.background.primary),
) {
val (iconRef, titleRef, cryptoAmountRef, fiatAmountRef, priceChangeRef, nonFiatContentRef) = createRefs()
val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden
?: (state as? TokenItemState.Draggable)?.isBalanceHidden
?: false
val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden ?: false
TokenIcon(
state = state.iconState,
modifier = Modifier.constrainAs(iconRef) {
centerVerticallyTo(parent)
start.linkTo(parent.start)
},
)
val density = LocalDensity.current
val titleRequiredMinWidth by remember(rootWidth) {
derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.22f) } }
}
TokenIcon(state = state.iconState, modifier = Modifier.layoutId(layoutId = LayoutId.ICON))
TokenTitle(
state = state.titleState,
modifier = Modifier
.layoutId(layoutId = LayoutId.TITLE)
.padding(horizontal = TangemTheme.dimens.spacing8)
.constrainAs(titleRef) {
start.linkTo(iconRef.end)
top.linkTo(parent.top)
width = Dimension.fillToConstraints.atLeast(dp = titleRequiredMinWidth)
when (state) {
is TokenItemState.Content -> end.linkTo(fiatAmountRef.start)
is TokenItemState.Draggable -> end.linkTo(nonFiatContentRef.start)
is TokenItemState.Unreachable,
is TokenItemState.NoAddress,
-> {
end.linkTo(nonFiatContentRef.start)
bottom.linkTo(parent.bottom)
}
else -> Unit
}
},
.padding(bottom = TangemTheme.dimens.spacing2),
)
TokenFiatAmount(
state = state.fiatAmountState,
isBalanceHidden = isBalanceHidden,
modifier = Modifier.constrainAs(fiatAmountRef) {
top.linkTo(parent.top)
end.linkTo(parent.end)
width = Dimension.fillToConstraints.atMostWrapContent
if (state is TokenItemState.Content) {
start.linkTo(titleRef.end)
}
},
modifier = Modifier
.layoutId(layoutId = LayoutId.FIAT_AMOUNT)
.padding(bottom = TangemTheme.dimens.spacing2),
)
val marginBetweenRows = TangemTheme.dimens.spacing2
TokenCryptoAmount(
state = state.cryptoAmountState,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing8)
.constrainAs(cryptoAmountRef) {
start.linkTo(iconRef.end)
top.linkTo(titleRef.bottom, marginBetweenRows)
bottom.linkTo(parent.bottom)
when (state) {
is TokenItemState.Content -> {
end.linkTo(priceChangeRef.start)
width = Dimension.fillToConstraints.atMostWrapContent
}
is TokenItemState.Draggable -> {
end.linkTo(nonFiatContentRef.start)
width = Dimension.fillToConstraints
}
else -> Unit
}
},
.layoutId(layoutId = LayoutId.CRYPTO_AMOUNT)
.padding(horizontal = TangemTheme.dimens.spacing8),
)
val priceChangeRequiredMinWidth by remember(rootWidth) {
derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.16f) } }
}
TokenPriceChange(
state = state.priceChangeState,
modifier = Modifier.constrainAs(priceChangeRef) {
top.linkTo(fiatAmountRef.bottom, marginBetweenRows)
end.linkTo(anchor = parent.end)
bottom.linkTo(parent.bottom)
when (state.priceChangeState) {
is TokenItemState.PriceChangeState.Content,
is TokenItemState.PriceChangeState.Unknown,
-> {
start.linkTo(cryptoAmountRef.end)
width = Dimension.fillToConstraints
.atLeast(priceChangeRequiredMinWidth)
}
else -> Unit
}
},
modifier = Modifier.layoutId(layoutId = LayoutId.PRICE_CHANGE),
)
NonFiatContentBlock(
state = state,
reorderableTokenListState = reorderableTokenListState,
modifier = Modifier.constrainAs(nonFiatContentRef) {
centerVerticallyTo(parent)
end.linkTo(parent.end)
},
)
}
}
@Composable
private inline fun BaseContainer(
modifier: Modifier = Modifier,
crossinline content: @Composable ConstraintLayoutScope.() -> Unit,
) {
Box(
modifier = modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size68)
.background(color = TangemTheme.colors.background.primary),
) {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.padding(all = TangemTheme.dimens.spacing14),
content = content,
modifier = Modifier.layoutId(layoutId = LayoutId.NON_FIAT_CONTENT),
)
}
}
@ -196,47 +107,299 @@ private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed
}
}
// region preview
@Preview
/**
* IMPORTANT! All margins that used between children setup like as children paddings.
*/
@Suppress("LongMethod")
@Composable
private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
val density = LocalDensity.current
val dimens = TangemTheme.dimens
Layout(content = content, modifier = modifier) { measurables, constraints ->
val layoutWidth = constraints.maxWidth
val layoutPadding = with(density) { dimens.size14.roundToPx() }
val layoutWidthWithPaddings = layoutWidth - 2 * layoutPadding
val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt()
val priceChangeMinWidth = (layoutWidth * PRICE_CHANGE_MIN_WIDTH_COEFFICIENT).toInt()
val icon = measurables.measure(layoutId = LayoutId.ICON, constraints = constraints)
/*
* Title width take the whole REMAINING space.
* If FiatAmount took the whole free space, then Title will has min width.
*/
val title: Placeable
// FiatAmount width must take the whole free space but is not greater the Title min size
var fiatAmount: Placeable? = null
// CryptoAmount width must take the whole free space but is not greater the PriceChange min size
var cryptoAmount: Placeable? = null
/*
* PriceChange width take the whole REMAINING space.
* If CryptoAmount took the whole free space, then PriceChange will has min width.
*/
val priceChange: Placeable?
val nonFiatContent = measurables.measure(layoutId = LayoutId.NON_FIAT_CONTENT, constraints = constraints)
var firstRowRemainingFreeSpace: Int? = null
var secondRowRemainingFreeSpace: Int? = null
when (state) {
is TokenItemState.Content,
is TokenItemState.Loading,
is TokenItemState.Locked,
-> {
fiatAmount = measurables.measureFiatAmount(
state = state,
maxWidth = layoutWidthWithPaddings - icon.width - titleMinWidth,
defaultConstraints = constraints,
)
cryptoAmount = measurables.measureCryptoAmount(
state = state,
maxWidth = layoutWidthWithPaddings - icon.width - priceChangeMinWidth,
defaultConstraints = constraints,
)
firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - fiatAmount.width
secondRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - cryptoAmount.width
}
is TokenItemState.Draggable -> {
cryptoAmount = measurables.measureCryptoAmount(
state = state,
maxWidth = layoutWidthWithPaddings - icon.width - nonFiatContent.width,
defaultConstraints = constraints,
)
firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - nonFiatContent.width
}
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> {
firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - nonFiatContent.width
}
}
title = measurables.measureTitle(
state = state,
minWidth = titleMinWidth,
remainingFreeSpace = firstRowRemainingFreeSpace,
defaultConstraints = constraints,
)
priceChange = secondRowRemainingFreeSpace?.let {
measurables.measurePriceChange(
state = state,
minWidth = priceChangeMinWidth,
remainingFreeSpace = secondRowRemainingFreeSpace,
defaultConstraints = constraints,
)
}
val layoutHeight = calculateLayoutHeight(
state = state,
minLayoutHeight = with(density) { dimens.size68.roundToPx() },
layoutPadding = layoutPadding,
betweenRowsPadding = with(density) { dimens.size2.roundToPx() },
title = title,
fiatAmount = fiatAmount,
cryptoAmount = cryptoAmount,
priceChange = priceChange,
)
layout(width = constraints.maxWidth, height = layoutHeight) {
icon.placeRelative(x = layoutPadding, y = (layoutHeight - icon.height).div(other = 2))
title.placeRelative(
x = layoutPadding + icon.width,
y = when (state) {
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> (layoutHeight - title.height).div(other = 2)
else -> layoutPadding
},
)
cryptoAmount?.placeRelative(
x = layoutPadding + icon.width,
y = layoutHeight - cryptoAmount.height - layoutPadding,
)
fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - layoutPadding, y = layoutPadding)
priceChange?.placeRelative(
x = layoutWidth - priceChange.width - layoutPadding,
y = layoutHeight - priceChange.height - layoutPadding,
)
nonFiatContent.placeRelative(
x = layoutWidth - nonFiatContent.width - layoutPadding,
y = (layoutHeight - nonFiatContent.height).div(other = 2),
)
}
}
}
private fun List<Measurable>.measureFiatAmount(
state: TokenItemState,
maxWidth: Int,
defaultConstraints: Constraints,
): Placeable {
return measure(
layoutId = LayoutId.FIAT_AMOUNT,
constraints = when (state) {
is TokenItemState.Content,
is TokenItemState.Draggable,
-> createConstrainsSafely(maxWidth = maxWidth)
else -> defaultConstraints
},
)
}
private fun List<Measurable>.measureCryptoAmount(
state: TokenItemState,
maxWidth: Int,
defaultConstraints: Constraints,
): Placeable {
return measure(
layoutId = LayoutId.CRYPTO_AMOUNT,
constraints = when (state) {
is TokenItemState.Content,
is TokenItemState.Draggable,
-> createConstrainsSafely(maxWidth = maxWidth)
else -> defaultConstraints
},
)
}
private fun List<Measurable>.measureTitle(
state: TokenItemState,
minWidth: Int,
remainingFreeSpace: Int,
defaultConstraints: Constraints,
): Placeable {
return measure(
layoutId = LayoutId.TITLE,
constraints = when (state) {
is TokenItemState.Content,
is TokenItemState.Draggable,
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> createDynamicConstrains(minWidth = minWidth, remainingFreeSpace = remainingFreeSpace)
else -> defaultConstraints
},
)
}
private fun List<Measurable>.measurePriceChange(
state: TokenItemState,
minWidth: Int,
remainingFreeSpace: Int,
defaultConstraints: Constraints,
): Placeable {
return measure(
layoutId = LayoutId.PRICE_CHANGE,
constraints = when (state) {
is TokenItemState.Content,
-> createDynamicConstrains(minWidth = minWidth, remainingFreeSpace = remainingFreeSpace)
else -> defaultConstraints
},
)
}
private fun List<Measurable>.measure(layoutId: LayoutId, constraints: Constraints): Placeable {
return requireNotNull(
value = firstOrNull { it.layoutId == layoutId },
lazyMessage = { "Measurables[$layoutId] is null" },
).measure(constraints)
}
private fun createDynamicConstrains(minWidth: Int, remainingFreeSpace: Int): Constraints {
return createConstrainsSafely(
minWidth = minWidth,
maxWidth = max(a = minWidth, b = remainingFreeSpace),
)
}
private fun createConstrainsSafely(
minWidth: Int = 0,
maxWidth: Int = Constraints.Infinity,
minHeight: Int = 0,
maxHeight: Int = Constraints.Infinity,
): Constraints {
return Constraints(
minWidth = minWidth.makeNotLessZero(),
maxWidth = maxWidth.makeNotLessZero(),
minHeight = minHeight.makeNotLessZero(),
maxHeight = maxHeight.makeNotLessZero(),
)
}
private fun Int.makeNotLessZero(): Int = max(a = 0, b = this)
@Suppress("LongParameterList")
private fun calculateLayoutHeight(
state: TokenItemState,
minLayoutHeight: Int,
layoutPadding: Int,
betweenRowsPadding: Int,
title: Placeable,
fiatAmount: Placeable?,
cryptoAmount: Placeable?,
priceChange: Placeable?,
): Int {
val firstColumnHeight: Int
val secondColumnHeight: Int
when (state) {
is TokenItemState.Content,
is TokenItemState.Loading,
is TokenItemState.Locked,
-> {
firstColumnHeight = 2 * layoutPadding + title.height + betweenRowsPadding + (cryptoAmount?.height ?: 0)
secondColumnHeight = 2 * layoutPadding + (fiatAmount?.height ?: 0) + betweenRowsPadding +
(priceChange?.height ?: 0)
}
is TokenItemState.Draggable,
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> {
firstColumnHeight = minLayoutHeight
secondColumnHeight = minLayoutHeight
}
}
return max(firstColumnHeight, secondColumnHeight).coerceAtLeast(minLayoutHeight)
}
@Preview(widthDp = 360)
@Composable
private fun Preview_CustomTokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) {
TangemTheme(isDark = false) {
TokenItem(state)
TokenItem(state = state)
}
}
@Preview
@Composable
private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
TangemTheme(isDark = true) {
TokenItem(state)
}
}
private class TokenConfigProvider : CollectionPreviewParameterProvider<TokenItemState>(
private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenItemState>(
collection = listOf(
WalletPreviewData.tokenItemVisibleState.copy(
cryptoAmountState = TokenItemState.CryptoAmountState.Content(
text = "5,41221467146712416241274127841274174213421 MATIC",
),
),
WalletPreviewData.tokenItemVisibleState.copy(
priceChangeState = TokenItemState.PriceChangeState.Content(
valueInPercent = "31231231231231231231223123123123212312312312.0%",
type = PriceChangeType.UP,
),
),
WalletPreviewData.tokenItemVisibleState.copy(
cryptoAmountState = TokenItemState.CryptoAmountState.Content(
text = "5,41221467146712416241274127841274174213421 MATIC",
),
priceChangeState = TokenItemState.PriceChangeState.Content(
valueInPercent = "31231231231231231231223123123123212312312312.0%",
type = PriceChangeType.UP,
),
),
WalletPreviewData.tokenItemVisibleState.copy(
iconState = WalletPreviewData.coinIconState.copy(showCustomBadge = true),
titleState = TokenItemState.TitleState.Content(
text = "PolygonPolygonPolygonPolygonPolygonPolygon",
hasPending = true,
),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3213123123321312312312312312 $"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,4123123213123123123123123123 MATIC"),
priceChangeState = TokenItemState.PriceChangeState.Content(
valueInPercent = "2365723643724723423742342374623642374723472342342.0%",
type = PriceChangeType.UP,
),
),
WalletPreviewData.tokenItemUnreachableState,
WalletPreviewData.tokenItemNoAddressState,
@ -247,6 +410,4 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider<TokenItem
WalletPreviewData.customTokenItemVisibleState,
WalletPreviewData.customTestnetTokenItemVisibleState,
),
)
// endregion preview
)

View file

@ -40,6 +40,7 @@ private fun ContentTitle(name: String, hasPending: Boolean, modifier: Modifier =
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing6),
verticalAlignment = Alignment.CenterVertically,
) {
/*
* If currency name has a long width, then it will completely displace the image.

View file

@ -52,7 +52,7 @@ internal sealed class TokenItemState {
override val id: String,
override val iconState: IconState,
override val titleState: TitleState,
override val fiatAmountState: FiatAmountState?,
override val fiatAmountState: FiatAmountState,
override val cryptoAmountState: CryptoAmountState.Content,
override val priceChangeState: PriceChangeState?,
val isBalanceHidden: Boolean,
@ -72,6 +72,7 @@ internal sealed class TokenItemState {
override val iconState: IconState,
override val titleState: TitleState,
override val cryptoAmountState: CryptoAmountState,
val isBalanceHidden: Boolean,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val priceChangeState: PriceChangeState? = null

View file

@ -11,6 +11,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeToken
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListHiddenStateConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListErrorConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListSortingErrorConverter
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.CryptoCurrencyToDraggableItemConverter
@ -24,12 +25,14 @@ internal class OrganizeTokensStateHolder(
private val intents: OrganizeTokensIntents,
private val dragAndDropIntents: DragAndDropIntents,
private val appCurrencyProvider: Provider<AppCurrency>,
private val isBalanceHiddenProvider: Provider<Boolean>,
private val listStateProvider: Provider<OrganizeTokensListState>,
) {
private val stateFlowInternal: MutableStateFlow<OrganizeTokensState> = MutableStateFlow(getInitialState())
private val tokenListConverter by lazy {
val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider)
val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider, isBalanceHiddenProvider)
val itemsConverter = TokenListToListStateConverter(
tokensConverter = tokensConverter,
groupsConverter = NetworkGroupToDraggableItemsConverter(tokensConverter),
@ -38,9 +41,8 @@ internal class OrganizeTokensStateHolder(
TokenListToStateConverter(Provider(stateFlowInternal::value), itemsConverter)
}
private val inProgressStateConverter by lazy {
InProgressStateConverter()
}
private val inProgressStateConverter by lazy { InProgressStateConverter() }
private val tokenListHiddenStateConverter by lazy { TokenListHiddenStateConverter(listStateProvider) }
private val tokenListErrorConverter by lazy {
TokenListErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter)
@ -80,6 +82,10 @@ internal class OrganizeTokensStateHolder(
updateState { copy(header = header.copy(isSortedByBalance = false)) }
}
fun updateHiddenState(isBalanceHidden: Boolean) {
updateState { copy(itemsState = tokenListHiddenStateConverter.convert(isBalanceHidden)) }
}
fun updateStateWithError(error: TokenListError) {
updateState { tokenListErrorConverter.convert(error) }
}

View file

@ -7,6 +7,8 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase
@ -36,6 +38,8 @@ internal class OrganizeTokensViewModel @Inject constructor(
private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase,
private val listenToFlipsUseCase: ListenToFlipsUseCase,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val dispatchers: CoroutineDispatcherProvider,
savedStateHandle: SavedStateHandle,
@ -45,6 +49,8 @@ internal class OrganizeTokensViewModel @Inject constructor(
private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow()
private var isBalanceHidden = true
private val dragAndDropAdapter = DragAndDropAdapter(
listStateProvider = Provider { uiState.value.itemsState },
)
@ -53,6 +59,8 @@ internal class OrganizeTokensViewModel @Inject constructor(
intents = this,
dragAndDropIntents = dragAndDropAdapter,
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
isBalanceHiddenProvider = Provider { isBalanceHidden },
listStateProvider = Provider { uiState.value.itemsState },
)
private val userWalletId: UserWalletId by lazy {
@ -68,6 +76,20 @@ internal class OrganizeTokensViewModel @Inject constructor(
override fun onCreate(owner: LifecycleOwner) {
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened)
isBalanceHiddenUseCase()
.flowWithLifecycle(owner.lifecycle)
.onEach { hidden ->
isBalanceHidden = hidden
stateHolder.updateHiddenState(isBalanceHidden)
}
.launchIn(viewModelScope)
viewModelScope.launch {
listenToFlipsUseCase()
.flowWithLifecycle(owner.lifecycle)
.collect()
}
bootstrapTokenList()
bootstrapDragAndDropUpdates()
}

View file

@ -19,4 +19,12 @@ internal sealed class OrganizeTokensListState {
object Empty : OrganizeTokensListState() {
override val items: PersistentList<DraggableItem> = persistentListOf()
}
fun copySealed(items: PersistentList<DraggableItem> = this.items): OrganizeTokensListState {
return when (this) {
is GroupedByNetwork -> copy(items = items)
is Ungrouped -> copy(items = items)
is Empty -> Empty
}
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error
import com.tangem.common.Provider
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
internal class TokenListHiddenStateConverter(
private val currentStateProvider: Provider<OrganizeTokensListState>,
) : Converter<Boolean, OrganizeTokensListState> {
override fun convert(input: Boolean): OrganizeTokensListState {
val currentState = currentStateProvider()
val isBalanceHidden = input
return currentState.copySealed(
currentState.items.map { draggableItem ->
if (draggableItem is DraggableItem.Token) {
draggableItem.copy(
tokenItemState = draggableItem.tokenItemState.copy(
isBalanceHidden = isBalanceHidden,
),
)
} else {
draggableItem
}
}.toPersistentList(),
)
}
}

View file

@ -13,26 +13,29 @@ import com.tangem.utils.converter.Converter
internal class CryptoCurrencyToDraggableItemConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
private val isBalanceHiddenProvider: Provider<Boolean>,
) : Converter<CryptoCurrencyStatus, DraggableItem.Token> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token {
return createDraggableToken(value, appCurrencyProvider())
return createDraggableToken(value, appCurrencyProvider(), isBalanceHiddenProvider())
}
override fun convertList(input: Collection<CryptoCurrencyStatus>): List<DraggableItem.Token> {
val appCurrency = appCurrencyProvider()
val isBalanceHidden = isBalanceHiddenProvider()
return input.map { createDraggableToken(it, appCurrency) }
return input.map { createDraggableToken(it, appCurrency, isBalanceHidden) }
}
private fun createDraggableToken(
currencyStatus: CryptoCurrencyStatus,
appCurrency: AppCurrency,
isBalanceHidden: Boolean,
): DraggableItem.Token {
return DraggableItem.Token(
tokenItemState = createTokenItemState(currencyStatus, appCurrency),
tokenItemState = createTokenItemState(currencyStatus, appCurrency, isBalanceHidden),
groupId = getGroupHeaderId(currencyStatus.currency.network),
)
}
@ -40,6 +43,7 @@ internal class CryptoCurrencyToDraggableItemConverter(
private fun createTokenItemState(
currencyStatus: CryptoCurrencyStatus,
appCurrency: AppCurrency,
isBalanceHidden: Boolean,
): TokenItemState.Draggable {
val currency = currencyStatus.currency
@ -52,6 +56,8 @@ internal class CryptoCurrencyToDraggableItemConverter(
} else {
TokenItemState.CryptoAmountState.Content(text = getFormattedFiatAmount(currencyStatus, appCurrency))
},
isBalanceHidden = isBalanceHidden,
)
}

View file

@ -2,9 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -28,16 +27,13 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
import androidx.constraintlayout.compose.Dimension
import androidx.compose.ui.unit.*
import androidx.constraintlayout.compose.*
import com.tangem.common.Strings
import com.tangem.core.ui.components.FontSizeRange
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.ResizableText
@ -50,6 +46,8 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
private const val HALF_OF_ITEM_WIDTH = 0.5
/**
* Wallet card
*
@ -67,47 +65,65 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) {
onRenameClick = { state.onRenameClick(state.id, it) },
isLockedState = state is WalletCardState.LockedContent,
modifier = modifier,
) {
val (title, balance, additionalText, image) = createRefs()
) { itemSize ->
val (titleRef, balanceRef, additionalTextRef, imageRef) = createRefs()
val contentVerticalMargin = TangemTheme.dimens.spacing12
Title(
state = state,
modifier = Modifier.constrainAs(title) {
TitleText(
text = state.title,
modifier = Modifier.constrainAs(titleRef) {
start.linkTo(parent.start)
top.linkTo(anchor = parent.top, margin = contentVerticalMargin)
end.linkTo(image.start)
end.linkTo(imageRef.start)
width = Dimension.fillToConstraints
},
)
val betweenContentMargin = TangemTheme.dimens.spacing8
var balanceWidth by remember { mutableStateOf(value = Int.MIN_VALUE) }
Balance(
state = state,
modifier = Modifier.constrainAs(balance) {
start.linkTo(parent.start)
top.linkTo(anchor = title.bottom, margin = betweenContentMargin)
bottom.linkTo(anchor = additionalText.top, margin = betweenContentMargin)
},
modifier = Modifier
.onSizeChanged { balanceWidth = it.width }
.padding(vertical = TangemTheme.dimens.spacing8)
.constrainAs(balanceRef) {
start.linkTo(parent.start)
top.linkTo(anchor = titleRef.bottom)
bottom.linkTo(anchor = additionalTextRef.top)
},
)
AdditionalInfo(
state = state,
modifier = Modifier.constrainAs(additionalText) {
text = resolveAdditionalTextByState(state),
modifier = Modifier.constrainAs(additionalTextRef) {
start.linkTo(parent.start)
top.linkTo(balanceRef.bottom)
bottom.linkTo(anchor = parent.bottom, margin = contentVerticalMargin)
when (state) {
is WalletCardState.Content,
is WalletCardState.Error,
is WalletCardState.HiddenContent,
-> {
end.linkTo(imageRef.start)
width = Dimension.fillToConstraints
}
else -> Unit
}
},
)
val imageWidth = TangemTheme.dimens.size120
// If balance has a large width then image must be hidden
val hasSpaceForImage by remember(key1 = balanceWidth, key2 = itemSize.width) {
mutableStateOf(value = balanceWidth < itemSize.width * HALF_OF_ITEM_WIDTH)
}
Image(
id = state.imageResId,
modifier = Modifier.constrainAs(image) {
centerVerticallyTo(parent)
top.linkTo(parent.top)
isVisible = hasSpaceForImage,
modifier = Modifier.constrainAs(imageRef) {
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
height = Dimension.fillToConstraints
width = Dimension.value(imageWidth)
},
)
}
@ -120,11 +136,11 @@ private fun CardContainer(
onRenameClick: (String) -> Unit,
isLockedState: Boolean,
modifier: Modifier = Modifier,
content: @Composable (ConstraintLayoutScope.() -> Unit),
content: @Composable (ConstraintLayoutScope.(IntSize) -> Unit),
) {
var isMenuVisible by rememberSaveable { mutableStateOf(value = false) }
var pressOffset by remember { mutableStateOf(value = DpOffset.Zero) }
var itemHeight by remember { mutableStateOf(value = 0.dp) }
var itemSize by remember { mutableStateOf(value = IntSize.Zero) }
val density = LocalDensity.current
val interactionSource = remember { MutableInteractionSource() }
@ -138,7 +154,7 @@ private fun CardContainer(
Modifier
} else {
Modifier
.onSizeChanged { itemHeight = with(density) { it.height.toDp() } }
.onSizeChanged { itemSize = it }
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.indication(interactionSource = interactionSource, indication = LocalIndication.current)
.pointerInput(true) {
@ -166,12 +182,15 @@ private fun CardContainer(
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing14),
) {
content()
content(itemSize)
}
}
var isRenameWalletDialogVisible by rememberSaveable { mutableStateOf(value = false) }
val itemHeight by remember(itemSize.height) {
mutableStateOf(value = with(density) { itemSize.height.toDp() })
}
ManageWalletContextMenu(
isMenuVisible = isMenuVisible,
pressOffset = pressOffset,
@ -243,22 +262,14 @@ private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClic
}
@Composable
private fun Title(state: WalletCardState, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
TitleText(title = state.title)
}
}
@Composable
private fun TitleText(title: String) {
private fun TitleText(text: String, modifier: Modifier = Modifier) {
Text(
text = title,
text = text,
modifier = modifier,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.button,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.button,
)
}
@ -269,6 +280,10 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) {
targetState = state,
label = "Update the balance",
modifier = modifier,
transitionSpec = {
fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) with
fadeOut(animationSpec = tween(durationMillis = 90))
},
) { walletCardState ->
when (walletCardState) {
is WalletCardState.Content -> {
@ -277,10 +292,12 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) {
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32),
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography.h2,
)
}
is WalletCardState.HiddenContent -> NonContentBalanceText(text = WalletCardState.HIDDEN_BALANCE_TEXT)
is WalletCardState.HiddenContent -> NonContentBalanceText(TextReference.Str(Strings.STARS))
is WalletCardState.Error -> NonContentBalanceText(text = WalletCardState.EMPTY_BALANCE_TEXT)
is WalletCardState.Loading -> {
RectangleShimmer(modifier = Modifier.nonContentBalanceSize(TangemTheme.dimens))
@ -309,29 +326,41 @@ private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier {
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun AdditionalInfo(state: WalletCardState, modifier: Modifier = Modifier) {
private fun AdditionalInfo(text: TextReference?, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = state,
targetState = text,
label = "Update the additional text",
modifier = modifier,
) { animatedState ->
when (animatedState) {
is WalletCardState.Content -> AdditionalInfoText(text = animatedState.additionalInfo)
is WalletCardState.HiddenContent -> AdditionalInfoText(text = animatedState.additionalInfo)
is WalletCardState.LockedContent -> AdditionalInfoText(text = animatedState.additionalInfo)
is WalletCardState.Error -> AdditionalInfoText(text = WalletCardState.EMPTY_BALANCE_TEXT)
is WalletCardState.Loading -> {
RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(dimens = TangemTheme.dimens))
}
transitionSpec = {
fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) with
fadeOut(animationSpec = tween(durationMillis = 90))
},
) { animatedText ->
if (animatedText != null) {
AdditionalInfoText(text = animatedText)
} else {
RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(dimens = TangemTheme.dimens))
}
}
}
private fun resolveAdditionalTextByState(state: WalletCardState): TextReference? {
return when (state) {
is WalletCardState.Content -> state.additionalInfo
is WalletCardState.LockedContent -> state.additionalInfo
is WalletCardState.Error -> WalletCardState.EMPTY_BALANCE_TEXT
is WalletCardState.HiddenContent -> WalletCardState.HIDDEN_BALANCE_TEXT
is WalletCardState.Loading -> null
}
}
@Composable
private fun AdditionalInfoText(text: TextReference) {
Text(
text = text.resolveReference(),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography.caption,
)
}
@ -351,11 +380,14 @@ private fun LockedContent(modifier: Modifier = Modifier) {
}
@Composable
private fun Image(@DrawableRes id: Int?, modifier: Modifier = Modifier) {
AnimatedVisibility(visible = id != null, modifier = modifier) {
private fun Image(@DrawableRes id: Int?, isVisible: Boolean, modifier: Modifier = Modifier) {
AnimatedVisibility(visible = id != null && isVisible, modifier = modifier) {
val imageRes = id ?: return@AnimatedVisibility
Image(
painter = painterResource(id = requireNotNull(id)),
painter = painterResource(id = imageRes),
contentDescription = null,
modifier = Modifier.width(width = TangemTheme.dimens.size120),
contentScale = ContentScale.FillWidth,
)
}

View file

@ -579,6 +579,7 @@ internal class WalletViewModel @Inject constructor(
getNetworkCoinStatusUseCase(
userWalletId = userWallet.walletId,
networkId = cryptoCurrencyStatus.currency.network.id,
derivationPath = cryptoCurrencyStatus.currency.network.derivationPath,
)
.take(count = 1)
.collectLatest {
@ -913,8 +914,26 @@ internal class WalletViewModel @Inject constructor(
private fun getSingleCurrencyContent(index: Int) {
val wallet = getWallet(index)
updatePrimaryCurrencyStatus(userWalletId = wallet.walletId)
updateNotifications(index)
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = wallet.walletId)
.distinctUntilChanged()
.onEach { maybeCryptoCurrencyStatus ->
uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus)
maybeCryptoCurrencyStatus.onRight { status ->
singleWalletCryptoCurrencyStatus = status
if (status.value.amount?.isZero() == false) {
setWalletWithFundsFoundUseCase()
}
updateNotifications(index)
updateButtons(userWalletId = wallet.walletId, currencyStatus = status)
updateTxHistory(status.currency)
}
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(marketPriceJobHolder)
}
private fun updateTxHistory(currency: CryptoCurrency) {
@ -935,28 +954,6 @@ internal class WalletViewModel @Inject constructor(
}
}
private fun updatePrimaryCurrencyStatus(userWalletId: UserWalletId) {
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId)
.distinctUntilChanged()
.onEach { maybeCryptoCurrencyStatus ->
uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus)
maybeCryptoCurrencyStatus.onRight { status ->
singleWalletCryptoCurrencyStatus = status
if (status.value.amount?.isZero() == false) {
setWalletWithFundsFoundUseCase()
}
updateButtons(userWalletId = userWalletId, currencyStatus = status)
updateTxHistory(status.currency)
}
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(marketPriceJobHolder)
}
private fun updateButtons(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) {
getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrencyStatus = currencyStatus)
.distinctUntilChanged()