Updated on 2026-08-14
This commit is contained in:
parent
24c7340558
commit
7a6ea23dd4
14 changed files with 816 additions and 59 deletions
|
|
@ -5,6 +5,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.features.feed.components.earn.DefaultEarnComponent
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
|
|
@ -12,6 +13,7 @@ import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams
|
|||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
|
||||
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
|
||||
|
|
@ -22,12 +24,15 @@ import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
|||
import kotlinx.serialization.Serializable
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeedEntryChildFactory @Inject constructor(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
private val portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
|
||||
private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory,
|
||||
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
|
||||
private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles,
|
||||
private val designFeatureToggles: DesignFeatureToggles,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
|
|
@ -77,6 +82,8 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
params = child.params,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
portfolioComponentFactory = portfolioComponentFactory,
|
||||
portfolioBlockComponentFactory = portfolioBlockComponentFactory,
|
||||
designFeatureToggles = designFeatureToggles,
|
||||
)
|
||||
}
|
||||
is Child.TokenList -> {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
|
|
@ -35,6 +36,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
|
||||
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
|
||||
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
|
||||
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
|
||||
|
|
@ -49,7 +51,9 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
appComponentContext: AppComponentContext,
|
||||
val params: Params,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
designFeatureToggles: DesignFeatureToggles,
|
||||
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
// applying l2 compatibility
|
||||
|
|
@ -61,25 +65,41 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
private val analyticsParams = params.analyticsParams
|
||||
private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams)
|
||||
|
||||
private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.shouldShowPortfolio) {
|
||||
portfolioComponentFactory.create(
|
||||
context = child("my_portfolio"),
|
||||
params = MarketsPortfolioComponent.Params(
|
||||
updatedParams.token,
|
||||
analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) },
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
private val portfolioComponent: MarketsPortfolioComponent? =
|
||||
if (updatedParams.shouldShowPortfolio && !designFeatureToggles.isRedesignEnabled) {
|
||||
portfolioComponentFactory.create(
|
||||
context = child("my_portfolio"),
|
||||
params = MarketsPortfolioComponent.Params(
|
||||
updatedParams.token,
|
||||
analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) },
|
||||
),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
private val portfolioBlockComponent: PortfolioBlockComponent? =
|
||||
if (updatedParams.shouldShowPortfolio && designFeatureToggles.isRedesignEnabled) {
|
||||
portfolioBlockComponentFactory.create(
|
||||
context = child("portfolio_block"),
|
||||
params = PortfolioBlockComponent.Params(updatedParams.token),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
componentScope.launch(dispatchers.default) {
|
||||
model.networksState.collectLatest { state ->
|
||||
when (state) {
|
||||
is TokenNetworksState.NetworksAvailable -> portfolioComponent?.setTokenNetworks(state.networks)
|
||||
TokenNetworksState.NoNetworksAvailable -> portfolioComponent?.setNoNetworksAvailable()
|
||||
is TokenNetworksState.NetworksAvailable -> {
|
||||
portfolioBlockComponent?.setTokenNetworks(state.networks)
|
||||
portfolioComponent?.setTokenNetworks(state.networks)
|
||||
}
|
||||
TokenNetworksState.NoNetworksAvailable -> {
|
||||
portfolioBlockComponent?.setNoNetworksAvailable()
|
||||
portfolioComponent?.setNoNetworksAvailable()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +212,11 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
component.Content(blockModifier)
|
||||
}
|
||||
},
|
||||
portfolioFloatingBlock = portfolioBlockComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
component.Content(blockModifier)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.markets.TokenMarketInfo
|
|||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Deprecated("Will be removed in favor of PortfolioComponent with redesigned UI")
|
||||
@Stable
|
||||
interface MarketsPortfolioComponent : ComposableContentComponent {
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockModel
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockRoute
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.ui.PortfolioBlock
|
||||
import com.tangem.features.feed.components.portfolio.PortfolioComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
internal class PortfolioBlockComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
|
||||
private val portfolioComponentFactory: PortfolioComponent.Factory = object : PortfolioComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: PortfolioComponent.Params): PortfolioComponent {
|
||||
TODO("STUB. Will be implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Params(
|
||||
val token: TokenMarketParams,
|
||||
)
|
||||
|
||||
private val model: PortfolioBlockModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = PortfolioBlockRoute.serializer(),
|
||||
handleBackButton = true,
|
||||
childFactory = ::createBottomSheetChild,
|
||||
)
|
||||
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
model.setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
fun setNoNetworksAvailable() {
|
||||
model.setNoNetworksAvailable()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
PortfolioBlock(modifier = modifier, state = state)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
@Suppress("UnusedParameter")
|
||||
private fun createBottomSheetChild(
|
||||
config: PortfolioBlockRoute,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent {
|
||||
val currencyId = model.cryptoCurrencyIdState.value ?: return ComposableBottomSheetComponent.EMPTY
|
||||
val portfolioComponent = portfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = PortfolioComponent.Params(id = currencyId),
|
||||
)
|
||||
return PortfolioBottomSheetWrapper(
|
||||
portfolioComponent = portfolioComponent,
|
||||
onDismiss = { model.bottomSheetNavigation.dismiss() },
|
||||
)
|
||||
}
|
||||
|
||||
private class PortfolioBottomSheetWrapper(
|
||||
private val portfolioComponent: PortfolioComponent,
|
||||
private val onDismiss: () -> Unit,
|
||||
) : ComposableBottomSheetComponent {
|
||||
|
||||
override fun dismiss() = onDismiss()
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
) {
|
||||
portfolioComponent.Content(modifier = Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext, params: Params): PortfolioBlockComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.model.PortfolioBlockModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface PortfolioBlockModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(PortfolioBlockModel::class)
|
||||
fun providePortfolioBlockModel(model: PortfolioBlockModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
|
||||
import com.tangem.common.getTotalFiatAmount
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.formatStyled
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.account.filterCryptoPortfolio
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.ui.state.PortfolioBlockUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class PortfolioBlockModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val allAccountSupplier: MultiAccountStatusListSupplier,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<PortfolioBlockUM>
|
||||
field = MutableStateFlow<PortfolioBlockUM>(PortfolioBlockUM.Loading)
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<PortfolioBlockRoute> = SlotNavigation()
|
||||
|
||||
val cryptoCurrencyIdState: StateFlow<CryptoCurrency.ID?>
|
||||
field = MutableStateFlow(null)
|
||||
|
||||
private val params = paramsContainer.require<PortfolioBlockComponent.Params>()
|
||||
private val currencyRawId: CryptoCurrency.RawID = params.token.id
|
||||
|
||||
private val tokenIcon: CurrencyIconState = CurrencyIconState.CoinIcon(
|
||||
url = params.token.imageUrl,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
)
|
||||
|
||||
private val availableNetworks = MutableSharedFlow<List<TokenMarketInfo.Network>>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
init {
|
||||
combineData()
|
||||
.onEach { state.value = it }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
availableNetworks.tryEmit(networks)
|
||||
}
|
||||
|
||||
fun setNoNetworksAvailable() {
|
||||
availableNetworks.tryEmit(emptyList())
|
||||
}
|
||||
|
||||
private fun combineData(): Flow<PortfolioBlockUM> {
|
||||
return availableNetworks.transformLatest { networks ->
|
||||
if (networks.isEmpty()) {
|
||||
emit(PortfolioBlockUM.Hidden)
|
||||
} else {
|
||||
emitAll(portfolioFlow().distinctUntilChanged())
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun portfolioFlow(): Flow<PortfolioBlockUM> {
|
||||
val portfolioDataFlow = allAccountSupplier().flatMapLatest { accountStatusLists ->
|
||||
val walletFlows = accountStatusLists.map { accountStatusList ->
|
||||
getUserWalletUseCase.invokeFlow(accountStatusList.userWalletId)
|
||||
.mapNotNull { it.getOrNull() }
|
||||
.map { wallet ->
|
||||
WalletPortfolio(
|
||||
userWallet = wallet,
|
||||
currencies = accountStatusList.filterByRawId(),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (walletFlows.isEmpty()) {
|
||||
flowOf(emptyList())
|
||||
} else {
|
||||
combine(walletFlows) { it.toList() }
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val settingsFlow = combine(
|
||||
getSelectedAppCurrencyUseCase.invokeOrDefault(),
|
||||
getBalanceHidingSettingsUseCase.isBalanceHidden(),
|
||||
) { appCurrency, isBalanceHidden ->
|
||||
SettingsBox(appCurrency, isBalanceHidden)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
return combine(portfolioDataFlow, settingsFlow) { portfolios, settings ->
|
||||
buildState(portfolios, settings)
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun AccountStatusList.filterByRawId(): List<CryptoCurrencyStatus> {
|
||||
return accountStatuses.filterCryptoPortfolio().flatMap { accountStatus ->
|
||||
accountStatus.tokenList.flattenCurrencies().filter { status ->
|
||||
val statusRawId = status.currency.id.rawCurrencyId ?: return@filter false
|
||||
getTokenIdIfL2Network(statusRawId.value) == currencyRawId.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildState(portfolios: List<WalletPortfolio>, settings: SettingsBox): PortfolioBlockUM {
|
||||
val allCurrencies = portfolios.flatMap { it.currencies }
|
||||
val hasMultiCurrencyWallet = portfolios.any { it.userWallet.isMultiCurrency }
|
||||
|
||||
if (allCurrencies.isEmpty()) {
|
||||
return if (hasMultiCurrencyWallet) {
|
||||
PortfolioBlockUM.AddToken(
|
||||
tokenIcon = tokenIcon,
|
||||
onClick = { bottomSheetNavigation.activate(PortfolioBlockRoute) },
|
||||
)
|
||||
} else {
|
||||
PortfolioBlockUM.Hidden
|
||||
}
|
||||
}
|
||||
|
||||
cryptoCurrencyIdState.update {
|
||||
it ?: allCurrencies.first().currency.id
|
||||
}
|
||||
|
||||
val totalFiat = allCurrencies.mapNotNull { it.getTotalFiatAmount() }
|
||||
.fold(BigDecimal.ZERO, BigDecimal::add)
|
||||
|
||||
val formattedBalance = totalFiat.formatStyled {
|
||||
fiat(
|
||||
fiatCurrencyCode = settings.appCurrency.code,
|
||||
fiatCurrencySymbol = settings.appCurrency.symbol,
|
||||
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
|
||||
)
|
||||
}
|
||||
|
||||
return PortfolioBlockUM.Content(
|
||||
totalBalance = formattedBalance,
|
||||
tokensInPortfolioCount = allCurrencies.size,
|
||||
tokenIcon = tokenIcon,
|
||||
tokenName = allCurrencies.first().currency.name,
|
||||
tokenSymbol = allCurrencies.first().currency.symbol,
|
||||
isBalanceHidden = settings.isBalanceHidden,
|
||||
onClick = { bottomSheetNavigation.activate(PortfolioBlockRoute) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class WalletPortfolio(
|
||||
val userWallet: UserWallet,
|
||||
val currencies: List<CryptoCurrencyStatus>,
|
||||
)
|
||||
|
||||
private data class SettingsBox(
|
||||
val appCurrency: AppCurrency,
|
||||
val isBalanceHidden: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock.model
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data object PortfolioBlockRoute : Route
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterExitState
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.ds.button.*
|
||||
import com.tangem.core.ui.ds.row.TangemRowContainer
|
||||
import com.tangem.core.ui.ds.row.TangemRowLayoutId
|
||||
import com.tangem.core.ui.extensions.formatAnnotatedWithBoldColor
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.feed.components.market.details.portfolioblock.ui.state.PortfolioBlockUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun PortfolioBlock(state: PortfolioBlockUM, modifier: Modifier = Modifier) {
|
||||
val isVisible = state is PortfolioBlockUM.AddToken || state is PortfolioBlockUM.Content
|
||||
val screenHeightPx = with(LocalDensity.current) { LocalWindowSize.current.height.toPx() }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter = fadeIn(animationSpec = tween(durationMillis = 300)),
|
||||
) {
|
||||
BottomFade(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
enter = fadeIn(animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)),
|
||||
exit = fadeOut(animationSpec = tween(durationMillis = 300)),
|
||||
) {
|
||||
val slideOffset by transition.animateFloat(
|
||||
transitionSpec = {
|
||||
tween(
|
||||
durationMillis = 600,
|
||||
easing = CubicBezierEasing(a = 0.83f, b = 0f, c = 0.17f, d = 1f),
|
||||
)
|
||||
},
|
||||
label = "slideOffset",
|
||||
) { enterExitState ->
|
||||
when (enterExitState) {
|
||||
EnterExitState.PreEnter -> screenHeightPx
|
||||
EnterExitState.Visible -> 0f
|
||||
EnterExitState.PostExit -> 0f
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.graphicsLayer { translationY = slideOffset }) {
|
||||
when (state) {
|
||||
is PortfolioBlockUM.AddToken -> AddTokenBlock(state)
|
||||
is PortfolioBlockUM.Content -> ContentBlock(state)
|
||||
is PortfolioBlockUM.Hidden,
|
||||
is PortfolioBlockUM.Loading,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = Modifier) {
|
||||
FloatingCard(
|
||||
modifier = modifier,
|
||||
onClick = state.onClick,
|
||||
) {
|
||||
TangemRowContainer(
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier
|
||||
.padding(end = TangemTheme.dimens2.x3)
|
||||
.layoutId(TangemRowLayoutId.HEAD),
|
||||
state = state.tokenIcon,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
|
||||
text = state.tokenName,
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
|
||||
text = stringResourceSafe(R.string.markets_portfolio_block_subtitle),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP),
|
||||
text = state.totalBalance.orMaskWithStars(state.isBalanceHidden).resolveAnnotatedReference(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
style = TangemTheme.typography2.bodyMedium16,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM),
|
||||
text = state.tokenSymbol,
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
TangemButton(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens2.x3)
|
||||
.layoutId(TangemRowLayoutId.TAIL),
|
||||
buttonUM = TangemButtonUM(
|
||||
type = TangemButtonType.Secondary,
|
||||
iconRes = R.drawable.ic_chevron_24,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
size = TangemButtonSize.X9,
|
||||
onClick = state.onClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddTokenBlock(state: PortfolioBlockUM.AddToken, modifier: Modifier = Modifier) {
|
||||
FloatingCard(
|
||||
modifier = modifier,
|
||||
onClick = state.onClick,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CurrencyIcon(state.tokenIcon)
|
||||
|
||||
SpacerW(TangemTheme.dimens2.x3)
|
||||
|
||||
Text(
|
||||
text = formatAnnotatedWithBoldColor(
|
||||
rawString = stringResourceSafe(R.string.markets_portfolio_block_add_token_title),
|
||||
boldColor = TangemTheme.colors2.text.neutral.primary,
|
||||
),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 2,
|
||||
)
|
||||
|
||||
SpacerW(TangemTheme.dimens2.x2)
|
||||
|
||||
TangemButton(
|
||||
buttonUM = TangemButtonUM(
|
||||
type = TangemButtonType.Secondary,
|
||||
text = resourceReference(R.string.common_add),
|
||||
shape = TangemButtonShape.Rounded,
|
||||
size = TangemButtonSize.X9,
|
||||
onClick = state.onClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun FloatingCard(modifier: Modifier = Modifier, onClick: () -> Unit = {}, content: @Composable () -> Unit) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.navigationBarsPadding()
|
||||
.fillMaxWidth()
|
||||
.widthIn(max = BottomSheetDefaults.SheetMaxWidth)
|
||||
.padding(
|
||||
start = TangemTheme.dimens2.x2,
|
||||
end = TangemTheme.dimens2.x2,
|
||||
bottom = TangemTheme.dimens2.x2,
|
||||
)
|
||||
.clip(RoundedCornerShape(size = TangemTheme.dimens2.x5))
|
||||
.background(
|
||||
color = TangemTheme.colors2.surface.level3,
|
||||
shape = RoundedCornerShape(size = TangemTheme.dimens2.x5),
|
||||
)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x3),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
PortfolioBlock(
|
||||
state = PortfolioBlockUM.Content(
|
||||
totalBalance = stringReference("$1,234.56"),
|
||||
tokensInPortfolioCount = 3,
|
||||
tokenIcon = previewCoinIcon,
|
||||
tokenName = "Bitcoin",
|
||||
tokenSymbol = "BTC",
|
||||
isBalanceHidden = false,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun AddTokenPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
PortfolioBlock(
|
||||
state = PortfolioBlockUM.AddToken(
|
||||
tokenIcon = previewCoinIcon,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val previewCoinIcon
|
||||
get() = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = com.tangem.core.ui.R.drawable.img_polygon_22,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolioblock.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class PortfolioBlockUM {
|
||||
|
||||
data object Loading : PortfolioBlockUM()
|
||||
data object Hidden : PortfolioBlockUM()
|
||||
|
||||
data class AddToken(
|
||||
val tokenIcon: CurrencyIconState,
|
||||
val onClick: () -> Unit,
|
||||
) : PortfolioBlockUM()
|
||||
|
||||
data class Content(
|
||||
val totalBalance: TextReference,
|
||||
val tokensInPortfolioCount: Int,
|
||||
val tokenIcon: CurrencyIconState,
|
||||
val tokenName: String,
|
||||
val tokenSymbol: String,
|
||||
val isBalanceHidden: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
) : PortfolioBlockUM()
|
||||
}
|
||||
|
|
@ -12,16 +12,22 @@ import androidx.compose.foundation.text.TextAutoSize
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
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.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
|
|
@ -64,6 +70,7 @@ internal fun MarketsTokenDetailsContent(
|
|||
backgroundColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
portfolioFloatingBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
Content(
|
||||
contentPadding = contentPadding,
|
||||
|
|
@ -71,6 +78,7 @@ internal fun MarketsTokenDetailsContent(
|
|||
backgroundColor = backgroundColor,
|
||||
state = state,
|
||||
portfolioBlock = portfolioBlock,
|
||||
portfolioFloatingBlock = portfolioFloatingBlock,
|
||||
)
|
||||
|
||||
when (state.bottomSheetConfig.content) {
|
||||
|
|
@ -80,7 +88,7 @@ internal fun MarketsTokenDetailsContent(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
private fun Content(
|
||||
contentPadding: PaddingValues,
|
||||
|
|
@ -88,6 +96,7 @@ private fun Content(
|
|||
backgroundColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
portfolioFloatingBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
val density = LocalDensity.current
|
||||
|
|
@ -97,54 +106,71 @@ private fun Content(
|
|||
lazyListState = lazyListState,
|
||||
onShouldShowPriceSubtitleChange = state.onShouldShowPriceSubtitleChange,
|
||||
)
|
||||
var bottomSpacing by remember { mutableStateOf(0.dp) }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
Box(
|
||||
modifier
|
||||
.drawBehind { drawRect(backgroundColor) }
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
SpacerH4()
|
||||
Column {
|
||||
SpacerH4()
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
|
||||
) {
|
||||
item("header") {
|
||||
Header(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
item("intervalSelector") {
|
||||
IntervalSelector(
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onSelectedIntervalChange,
|
||||
isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH32() }
|
||||
item("chart") {
|
||||
MarketTokenDetailsChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = backgroundColor,
|
||||
state = state.chartState,
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
|
||||
) {
|
||||
item("header") {
|
||||
Header(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
item("intervalSelector") {
|
||||
IntervalSelector(
|
||||
trendInterval = state.selectedInterval,
|
||||
onIntervalClick = state.onSelectedIntervalChange,
|
||||
isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
item { SpacerH32() }
|
||||
item("chart") {
|
||||
MarketTokenDetailsChart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = backgroundColor,
|
||||
state = state.chartState,
|
||||
)
|
||||
}
|
||||
item { SpacerH16() }
|
||||
|
||||
tokenMarketDetailsBody(
|
||||
state = state.body,
|
||||
portfolioBlock = portfolioBlock,
|
||||
relatedNews = state.relatedNews,
|
||||
isRedesignEnabled = isRedesignEnabled,
|
||||
)
|
||||
tokenMarketDetailsBody(
|
||||
state = state.body,
|
||||
portfolioBlock = portfolioBlock,
|
||||
relatedNews = state.relatedNews,
|
||||
isRedesignEnabled = isRedesignEnabled,
|
||||
)
|
||||
|
||||
item { SpacerH(bottomSpacing) }
|
||||
}
|
||||
}
|
||||
|
||||
portfolioFloatingBlock?.invoke(
|
||||
Modifier
|
||||
.onSizeChanged { size ->
|
||||
bottomSpacing = if (size.height > 0) {
|
||||
with(density) { size.height.toDp() + 16.dp }
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
}
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -404,6 +430,7 @@ private fun MarketsTokenDetailsContent_Preview(
|
|||
state = params,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
portfolioBlock = {},
|
||||
portfolioFloatingBlock = null,
|
||||
contentPadding = PaddingValues(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue