Updated on 2026-08-14
This commit is contained in:
commit
870c3fa9d3
291 changed files with 4985 additions and 5870 deletions
|
|
@ -22,6 +22,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
import com.tangem.features.feed.components.earn.DefaultEarnComponent
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||
|
|
@ -131,6 +132,14 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
override fun onOpenAllNews() {
|
||||
innerRouter.push(FeedEntryChildFactory.Child.NewsList)
|
||||
}
|
||||
|
||||
override fun onOpenEarnPage() {
|
||||
innerRouter.push(
|
||||
FeedEntryChildFactory.Child.Earn(
|
||||
params = DefaultEarnComponent.Params(onBackClick = { onChildBack() }),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>> =
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
|||
import com.tangem.features.feed.components.earn.DefaultEarnComponent
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
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.list.DefaultMarketsTokenListComponent
|
||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||
|
|
@ -21,6 +22,7 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
|
|
@ -100,6 +102,7 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
DefaultFeedComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents),
|
||||
addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent,
|
||||
)
|
||||
}
|
||||
is Child.Earn -> {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,16 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
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.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.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
|
||||
import com.tangem.features.feed.model.feed.FeedComponentModel
|
||||
import com.tangem.features.feed.model.feed.FeedModelClickIntents
|
||||
import com.tangem.features.feed.ui.feed.FeedList
|
||||
|
|
@ -18,10 +24,18 @@ import com.tangem.features.feed.ui.feed.FeedListHeader
|
|||
internal class DefaultFeedComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: FeedParams,
|
||||
private val addToPortfolioComponentFactory: AddToPortfolioPreselectedDataComponent.Factory,
|
||||
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val feedComponentModel = getOrCreateModel<FeedComponentModel, FeedParams>(params = params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = feedComponentModel.bottomSheetNavigation,
|
||||
serializer = FeedPortfolioRoute.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val state by feedComponentModel.state.collectAsStateWithLifecycle()
|
||||
|
|
@ -40,11 +54,28 @@ internal class DefaultFeedComponent(
|
|||
}
|
||||
}
|
||||
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
val state by feedComponentModel.state.collectAsStateWithLifecycle()
|
||||
FeedList(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: FeedPortfolioRoute,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = when (config) {
|
||||
is FeedPortfolioRoute.AddToPortfolio -> {
|
||||
addToPortfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddToPortfolioPreselectedDataComponent.Params(
|
||||
tokenToAdd = config.tokenToAdd,
|
||||
callback = feedComponentModel.addToPortfolioCallback,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class FeedParams(val feedClickIntents: FeedModelClickIntents)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.feed.components.feed
|
||||
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface FeedPortfolioRoute {
|
||||
|
||||
@Serializable
|
||||
data class AddToPortfolio(val tokenToAdd: AddToPortfolioPreselectedDataComponent.TokenToAdd) : FeedPortfolioRoute
|
||||
}
|
||||
|
|
@ -95,6 +95,7 @@ internal class DefaultMarketsTokenDetailsComponent(
|
|||
tokenName = state.tokenName,
|
||||
tokenPrice = state.priceText,
|
||||
backgroundColor = background,
|
||||
onShareClick = state.onShareClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package com.tangem.features.feed.components.market.details.portfolio.add.impl.di
|
|||
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.DefaultAddToPortfolioComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.DefaultAddToPortfolioPreselectedDataComponent
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.DefaultAddToPortfolioManager
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -18,4 +20,9 @@ internal interface AddToPortfolioComponentModule {
|
|||
|
||||
@Binds
|
||||
fun bindAddToPortfolioManagerFactory(factory: DefaultAddToPortfolioManager.Factory): AddToPortfolioManager.Factory
|
||||
|
||||
@Binds
|
||||
fun bindAddToPortfolioPreselectedDataComponent(
|
||||
factory: DefaultAddToPortfolioPreselectedDataComponent.Factory,
|
||||
): AddToPortfolioPreselectedDataComponent.Factory
|
||||
}
|
||||
|
|
@ -112,8 +112,10 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
|
|||
// suspend until all required data is selected
|
||||
val (selectedNetworkValue, selectedPortfolioValue) = allRequireForAdd.first()
|
||||
|
||||
val isTokenAlreadyAdded = selectedPortfolioValue.account.addedMarketNetworks
|
||||
.any { it.networkId == selectedNetworkValue.selectedNetwork.networkId }
|
||||
val isTokenAlreadyAdded = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = selectedPortfolioValue.userWallet.walletId,
|
||||
currency = selectedNetworkValue.cryptoCurrency,
|
||||
).isSome()
|
||||
|
||||
if (isTokenAlreadyAdded) {
|
||||
finishSuccessFlow(
|
||||
|
|
@ -165,7 +167,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
|
|||
): AvailableToAddData? {
|
||||
val portfolioData = portfolioFetcher.data.firstOrNull() ?: return null
|
||||
|
||||
val availableToOpenWallets = portfolioData.balances.mapNotNull { (walletId, balance) ->
|
||||
val availableToAddInWallets = portfolioData.balances.mapNotNull { (walletId, balance) ->
|
||||
val wallet = balance.userWallet
|
||||
val accounts = balance.accountsBalance.accountStatuses
|
||||
|
||||
|
|
@ -206,9 +208,9 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
|
|||
)
|
||||
}.toMap()
|
||||
|
||||
if (availableToOpenWallets.isEmpty()) return null
|
||||
if (availableToAddInWallets.isEmpty()) return null
|
||||
|
||||
return AvailableToAddData(availableToAddWallets = availableToOpenWallets)
|
||||
return AvailableToAddData(availableToAddWallets = availableToAddInWallets)
|
||||
}
|
||||
|
||||
private suspend fun createCryptoCurrency(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
|
|
@ -17,6 +18,7 @@ import com.tangem.core.ui.haptic.TangemHapticEffect
|
|||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.MarketTokenDetailsBottomSheetTestTags
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
import com.tangem.features.feed.impl.R
|
||||
|
|
@ -49,6 +51,7 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie
|
|||
start = TangemTheme.dimens.spacing10,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
modifier = Modifier.testTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_TOKEN_ITEM),
|
||||
)
|
||||
|
||||
PortfolioQuickActions(
|
||||
|
|
|
|||
|
|
@ -1,45 +1,29 @@
|
|||
package com.tangem.features.feed.components.market.details.portfolio.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -51,6 +35,7 @@ import com.tangem.core.ui.haptic.TangemHapticEffect
|
|||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.MarketTokenDetailsBottomSheetTestTags
|
||||
import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.QuickActionUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -151,7 +136,8 @@ private fun AnimatedVisibilityScope.QuickActionItem(
|
|||
onClick()
|
||||
},
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4),
|
||||
.padding(horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4)
|
||||
.testTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_QUICK_ACTION_BUTTON),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18),
|
||||
) {
|
||||
|
|
@ -168,6 +154,7 @@ private fun AnimatedVisibilityScope.QuickActionItem(
|
|||
text = state.title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.testTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_QUICK_ACTION_BUTTON_TITLE),
|
||||
)
|
||||
Text(
|
||||
text = state.description.resolveReference(),
|
||||
|
|
@ -192,6 +179,13 @@ private fun AnimatedVisibilityScope.QuickActionIcon(state: QuickActionUM) {
|
|||
shape = CircleShape,
|
||||
)
|
||||
.size(TangemTheme.dimens.size32)
|
||||
.semantics {
|
||||
contentDescription = if (state is QuickActionUM.Exchange && state.shouldShowBadge) {
|
||||
"Badge shown"
|
||||
} else {
|
||||
"Badge hidden"
|
||||
}
|
||||
}
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
if (state is QuickActionUM.Exchange && state.shouldShowBadge) {
|
||||
|
|
@ -202,7 +196,8 @@ private fun AnimatedVisibilityScope.QuickActionIcon(state: QuickActionUM) {
|
|||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.requiredSize(TangemTheme.dimens.size16),
|
||||
.requiredSize(TangemTheme.dimens.size16)
|
||||
.testTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_QUICK_ACTION_BUTTON_ICON),
|
||||
imageVector = ImageVector.vectorResource(id = state.icon),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.button.primary,
|
||||
|
|
|
|||
|
|
@ -4,13 +4,24 @@ import androidx.compose.runtime.Stable
|
|||
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.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.feed.components.earn.DefaultEarnComponent
|
||||
import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
|
|
@ -22,18 +33,213 @@ internal class EarnModel @Inject constructor(
|
|||
|
||||
private val params = paramsContainer.require<DefaultEarnComponent.Params>()
|
||||
|
||||
// TODO [REDACTED_TASK_KEY] remove mocks
|
||||
private val mockNetworks: ImmutableList<EarnFilterNetworkUM> = persistentListOf(
|
||||
EarnFilterNetworkUM.AllNetworks(
|
||||
text = resourceReference(R.string.earn_filter_all_networks),
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.MyNetworks(
|
||||
text = resourceReference(R.string.earn_filter_my_networks),
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.Network(
|
||||
id = "ethereum",
|
||||
text = stringReference("Ethereum"),
|
||||
symbol = stringReference("ETH"),
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/ethereum.png",
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.Network(
|
||||
id = "polygon",
|
||||
text = stringReference("Polygon"),
|
||||
symbol = stringReference("MATIC"),
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/matic-network.png",
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.Network(
|
||||
id = "base",
|
||||
text = stringReference("Base"),
|
||||
symbol = stringReference("BASE"),
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/base.png",
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.Network(
|
||||
id = "avalanche",
|
||||
text = stringReference("Avalance"),
|
||||
symbol = stringReference("AVAX"),
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/avalanche-2.png",
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.Network(
|
||||
id = "cardano",
|
||||
text = stringReference("Cardano"),
|
||||
symbol = stringReference("ADA"),
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/cardano.png",
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.Network(
|
||||
id = "bsc",
|
||||
text = stringReference("Bitcoin Smart Chain"),
|
||||
symbol = stringReference("BSC"),
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/binancecoin.png",
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.Network(
|
||||
id = "bitcoin",
|
||||
text = stringReference("Bitcoin"),
|
||||
symbol = stringReference("BTC"),
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/bitcoin.png",
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.Network(
|
||||
id = "solana",
|
||||
text = stringReference("Solana"),
|
||||
symbol = stringReference("SOL"),
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/solana.png",
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.Network(
|
||||
id = "ton",
|
||||
text = stringReference("Ton"),
|
||||
symbol = stringReference("TON"),
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/the-open-network.png",
|
||||
isSelected = false,
|
||||
),
|
||||
)
|
||||
|
||||
val state: StateFlow<EarnUM>
|
||||
field = MutableStateFlow(createInitialState())
|
||||
|
||||
private fun createInitialState(): EarnUM = EarnUM(
|
||||
mostlyUsed = EarnListUM.Loading,
|
||||
bestOpportunities = EarnListUM.Loading,
|
||||
selectedNetworkFilter = null,
|
||||
selectedTypeFilter = null,
|
||||
networkFilters = persistentListOf(),
|
||||
typeFilters = persistentListOf(),
|
||||
onBackClick = params.onBackClick,
|
||||
onNetworkFilterClick = { },
|
||||
onTypeFilterClick = { },
|
||||
)
|
||||
private fun createInitialState(): EarnUM {
|
||||
val defaultNetworkFilter = mockNetworks.first()
|
||||
val defaultTypeFilter = EarnFilterTypeUM.All
|
||||
|
||||
return EarnUM(
|
||||
mostlyUsed = EarnListUM.Loading,
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
selectedTypeFilter = defaultTypeFilter,
|
||||
selectedNetworkFilter = defaultNetworkFilter,
|
||||
filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(defaultTypeFilter),
|
||||
filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(defaultNetworkFilter),
|
||||
onBackClick = params.onBackClick,
|
||||
onNetworkFilterClick = ::onNetworkFilterClick,
|
||||
onTypeFilterClick = ::onTypeFilterClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTypeFilterBottomSheetConfig(
|
||||
selectedOption: EarnFilterTypeUM,
|
||||
isShown: Boolean = false,
|
||||
): TangemBottomSheetConfig {
|
||||
return TangemBottomSheetConfig(
|
||||
isShown = isShown,
|
||||
onDismissRequest = ::hideTypeFilterBottomSheet,
|
||||
content = EarnFilterByTypeBottomSheetContentUM(
|
||||
selectedOption = selectedOption,
|
||||
onOptionClick = ::onTypeFilterOptionClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNetworkFilterBottomSheetConfig(
|
||||
selectedNetwork: EarnFilterNetworkUM,
|
||||
isShown: Boolean = false,
|
||||
): TangemBottomSheetConfig {
|
||||
val updatedNetworks = mockNetworks.map { network ->
|
||||
when {
|
||||
network is EarnFilterNetworkUM.AllNetworks && selectedNetwork is EarnFilterNetworkUM.AllNetworks ->
|
||||
network.copy(isSelected = true)
|
||||
network is EarnFilterNetworkUM.MyNetworks && selectedNetwork is EarnFilterNetworkUM.MyNetworks ->
|
||||
network.copy(isSelected = true)
|
||||
network is EarnFilterNetworkUM.Network && selectedNetwork is EarnFilterNetworkUM.Network &&
|
||||
network.id == selectedNetwork.id ->
|
||||
network.copy(isSelected = true)
|
||||
else -> network
|
||||
}
|
||||
}
|
||||
|
||||
return TangemBottomSheetConfig(
|
||||
isShown = isShown,
|
||||
onDismissRequest = ::hideNetworkFilterBottomSheet,
|
||||
content = EarnFilterByNetworkBottomSheetContentUM(
|
||||
selectedNetwork = selectedNetwork,
|
||||
networks = persistentListOf(*updatedNetworks.toTypedArray()),
|
||||
onOptionClick = ::onNetworkFilterOptionClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onTypeFilterClick() {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(
|
||||
selectedOption = currentState.selectedTypeFilter,
|
||||
isShown = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNetworkFilterClick() {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(
|
||||
selectedNetwork = currentState.selectedNetworkFilter,
|
||||
isShown = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideTypeFilterBottomSheet() {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
filterByTypeBottomSheet = currentState.filterByTypeBottomSheet.copy(isShown = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideNetworkFilterBottomSheet() {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
filterByNetworkBottomSheet = currentState.filterByNetworkBottomSheet.copy(isShown = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTypeFilterOptionClick(type: EarnFilterTypeUM) {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
selectedTypeFilter = type,
|
||||
filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(selectedOption = type),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNetworkFilterOptionClick(network: EarnFilterNetworkUM) {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
selectedNetworkFilter = network,
|
||||
filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(selectedNetwork = network),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [REDACTED_TASK_KEY] remove
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun onClearFiltersClick() {
|
||||
val defaultNetworkFilter = mockNetworks.first()
|
||||
val defaultTypeFilter = EarnFilterTypeUM.All
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
selectedTypeFilter = defaultTypeFilter,
|
||||
selectedNetworkFilter = defaultNetworkFilter,
|
||||
filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(selectedOption = defaultTypeFilter),
|
||||
filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(
|
||||
selectedNetwork = defaultNetworkFilter,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,11 @@ package com.tangem.features.feed.model.feed
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
|
@ -12,20 +17,28 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.earn.usecase.FetchTopEarnTokensUseCase
|
||||
import com.tangem.domain.earn.usecase.GetTopEarnTokensUseCase
|
||||
import com.tangem.domain.markets.GetTopFiveMarketTokenUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
import com.tangem.domain.markets.toSerializableParam
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.earn.EarnTokenWithCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase
|
||||
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
import com.tangem.features.feed.components.feed.FeedPortfolioRoute
|
||||
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
|
||||
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent
|
||||
import com.tangem.features.feed.model.feed.state.FeedMarketsBatchFlowManager
|
||||
import com.tangem.features.feed.model.feed.state.FeedStateController
|
||||
import com.tangem.features.feed.model.feed.state.transformers.UpdateGlobalFeedStateTransformer
|
||||
import com.tangem.features.feed.model.feed.state.transformers.UpdateMarketChartsTransformer
|
||||
import com.tangem.features.feed.model.feed.state.transformers.UpdateTrendingNewsStateTransformer
|
||||
import com.tangem.features.feed.model.feed.state.transformers.*
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -41,22 +54,26 @@ import javax.inject.Inject
|
|||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class FeedComponentModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase,
|
||||
private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val stateController: FeedStateController,
|
||||
private val feedFeatureToggle: FeedFeatureToggle,
|
||||
private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase,
|
||||
private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase,
|
||||
private val appRouter: AppRouter,
|
||||
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<DefaultFeedComponent.FeedParams>()
|
||||
|
||||
private var quotesUpdateJob: Job? = null
|
||||
|
||||
private val params = paramsContainer.require<DefaultFeedComponent.FeedParams>()
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}.stateIn(
|
||||
|
|
@ -72,7 +89,22 @@ internal class FeedComponentModel @Inject constructor(
|
|||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
internal val state: StateFlow<FeedListUM>
|
||||
val bottomSheetNavigation: SlotNavigation<FeedPortfolioRoute> = SlotNavigation()
|
||||
|
||||
val addToPortfolioCallback = object : AddToPortfolioPreselectedDataComponent.Callback {
|
||||
override fun onDismiss() = bottomSheetNavigation.dismiss()
|
||||
override fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) {
|
||||
bottomSheetNavigation.dismiss()
|
||||
appRouter.push(
|
||||
AppRoute.CurrencyDetails(
|
||||
userWalletId = walletId,
|
||||
currency = addedToken,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val state: StateFlow<FeedListUM>
|
||||
get() = stateController.uiState
|
||||
|
||||
val isVisibleOnScreen = MutableStateFlow(false)
|
||||
|
|
@ -81,29 +113,38 @@ internal class FeedComponentModel @Inject constructor(
|
|||
initializeState()
|
||||
updateCallbacks()
|
||||
fetchTrendingNews()
|
||||
fetchEarnData()
|
||||
fetchCharts()
|
||||
subscribeOnCurrencyUpdate()
|
||||
loadCharts()
|
||||
subscribeOnDataState()
|
||||
}
|
||||
|
||||
private fun subscribeOnDataState() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
combine(
|
||||
flow = marketsBatchFlowManager.itemsByOrder,
|
||||
flow2 = marketsBatchFlowManager.loadingStatesByOrder,
|
||||
flow3 = marketsBatchFlowManager.errorStatesByOrder,
|
||||
flow4 = manageTrendingNewsUseCase.observeTrendingNews(),
|
||||
) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult ->
|
||||
flow5 = getTopEarnTokensUseCase(),
|
||||
) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult, earnResult ->
|
||||
val globalStateTransformer = UpdateGlobalFeedStateTransformer(
|
||||
loadingStatesByOrder = loadingStatesByOrder,
|
||||
errorStatesByOrder = errorStatesByOrder,
|
||||
trendingNewsResult = trendingNewsResult,
|
||||
earnResult = earnResult,
|
||||
onRetryClicked = {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
stateController.update { currentState ->
|
||||
currentState.copy(globalState = GlobalFeedState.Loading)
|
||||
}
|
||||
fetchTrendingNewsUseCase.invoke()
|
||||
fetchTrendingNews()
|
||||
fetchEarnData()
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
}
|
||||
},
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
feedFeatureToggle = feedFeatureToggle,
|
||||
)
|
||||
|
||||
val currentState = stateController.value
|
||||
|
|
@ -123,13 +164,15 @@ internal class FeedComponentModel @Inject constructor(
|
|||
),
|
||||
UpdateTrendingNewsStateTransformer(
|
||||
result = trendingNewsResult,
|
||||
onRetryClicked = {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase.invoke()
|
||||
}
|
||||
},
|
||||
onRetryClicked = ::fetchTrendingNews,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
),
|
||||
UpdateEarnStateTransformer(
|
||||
isEarnEnabled = feedFeatureToggle.isEarnBlockEnabled,
|
||||
onItemClick = ::handleEarnTokenClick,
|
||||
onRetryClick = ::fetchEarnData,
|
||||
earnResult = earnResult,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -144,12 +187,6 @@ internal class FeedComponentModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun fetchTrendingNews() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase()
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrencyUpdate() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
currentAppCurrency.drop(1).collect {
|
||||
|
|
@ -158,7 +195,21 @@ internal class FeedComponentModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun loadCharts() {
|
||||
private fun fetchTrendingNews() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchEarnData() {
|
||||
if (!feedFeatureToggle.isEarnBlockEnabled) return
|
||||
modelScope.launch(dispatchers.default) {
|
||||
stateController.update(UpdateEarnLoadingStateTransformer())
|
||||
fetchTopEarnTokensUseCase()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchCharts() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
TokenMarketListConfig.Order.entries.forEach { order ->
|
||||
marketsBatchFlowManager.getOnLastBatchLoadedSuccessFlow(order)?.collect { batchKey ->
|
||||
|
|
@ -198,6 +249,7 @@ internal class FeedComponentModel @Inject constructor(
|
|||
onSliderEndReached = {
|
||||
analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselEndReached())
|
||||
},
|
||||
onOpenEarnPageClick = params.feedClickIntents::onOpenEarnPage,
|
||||
),
|
||||
news = NewsUM(
|
||||
content = persistentListOf(),
|
||||
|
|
@ -214,14 +266,41 @@ internal class FeedComponentModel @Inject constructor(
|
|||
currentSortByType = SortByTypeUM.TopGainers,
|
||||
),
|
||||
globalState = GlobalFeedState.Loading,
|
||||
earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) {
|
||||
EarnListUM.Loading
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCurrentDate(): String {
|
||||
val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault())
|
||||
return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate)
|
||||
private fun startQuotesUpdateTimer() {
|
||||
quotesUpdateJob?.cancel()
|
||||
quotesUpdateJob = modelScope.launch {
|
||||
while (true) {
|
||||
delay(DELAY_TO_FETCH_QUOTES)
|
||||
isVisibleOnScreen.first { it }
|
||||
marketsBatchFlowManager.updateQuotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCallbacks() {
|
||||
stateController.update { feedListUM ->
|
||||
feedListUM.copy(
|
||||
feedListCallbacks = feedListUM.feedListCallbacks.copy(
|
||||
onSortTypeClick = ::handleSortTypeClicked,
|
||||
onMarketItemClick = ::handleMarketItemClicked,
|
||||
onMarketOpenClick = ::handleMarketOpenClicked,
|
||||
onArticleClick = ::handleArticleClicked,
|
||||
onOpenAllNews = ::handleOpenAllNews,
|
||||
onOpenEarnPageClick = params.feedClickIntents::onOpenEarnPage,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* start of clicks area */
|
||||
private fun handleSortTypeClicked(sortByType: SortByTypeUM) {
|
||||
stateController.update { currentState ->
|
||||
val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) ->
|
||||
|
|
@ -249,31 +328,6 @@ internal class FeedComponentModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun startQuotesUpdateTimer() {
|
||||
quotesUpdateJob?.cancel()
|
||||
quotesUpdateJob = modelScope.launch {
|
||||
while (true) {
|
||||
delay(DELAY_TO_FETCH_QUOTES)
|
||||
isVisibleOnScreen.first { it }
|
||||
marketsBatchFlowManager.updateQuotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCallbacks() {
|
||||
stateController.update { feedListUM ->
|
||||
feedListUM.copy(
|
||||
feedListCallbacks = feedListUM.feedListCallbacks.copy(
|
||||
onSortTypeClick = ::handleSortTypeClicked,
|
||||
onMarketItemClick = ::handleMarketItemClicked,
|
||||
onMarketOpenClick = ::handleMarketOpenClicked,
|
||||
onArticleClick = ::handleArticleClicked,
|
||||
onOpenAllNews = ::handleOpenAllNews,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMarketItemClicked(item: MarketsListItemUM) {
|
||||
val tokenMarket = marketsBatchFlowManager.getTokenMarketById(item.id)
|
||||
if (tokenMarket != null) {
|
||||
|
|
@ -342,6 +396,26 @@ internal class FeedComponentModel @Inject constructor(
|
|||
params.feedClickIntents.onOpenAllNews()
|
||||
}
|
||||
|
||||
private fun handleEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency) {
|
||||
bottomSheetNavigation.activate(
|
||||
FeedPortfolioRoute.AddToPortfolio(
|
||||
tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd(
|
||||
network = TokenMarketInfo.Network(
|
||||
networkId = earnTokenWithCurrency.earnToken.networkId,
|
||||
isExchangeable = false,
|
||||
contractAddress = earnTokenWithCurrency.earnToken.tokenAddress,
|
||||
decimalCount = earnTokenWithCurrency.earnToken.decimalCount,
|
||||
),
|
||||
id = CryptoCurrency.RawID(earnTokenWithCurrency.earnToken.tokenId),
|
||||
name = earnTokenWithCurrency.earnToken.tokenName,
|
||||
symbol = earnTokenWithCurrency.earnToken.tokenSymbol,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
/* end of clicks area */
|
||||
|
||||
/* start of utils area */
|
||||
private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order {
|
||||
return when (this) {
|
||||
SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating
|
||||
|
|
@ -354,6 +428,12 @@ internal class FeedComponentModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getCurrentDate(): String {
|
||||
val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault())
|
||||
return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate)
|
||||
}
|
||||
/* end of utils area */
|
||||
|
||||
companion object {
|
||||
private const val DELAY_TO_FETCH_QUOTES = 60_000L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,4 +27,6 @@ internal interface FeedModelClickIntents {
|
|||
paginationConfig: NewsListConfig? = null,
|
||||
)
|
||||
fun onOpenAllNews()
|
||||
|
||||
fun onOpenEarnPage()
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.features.feed.model.feed.state
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
||||
import com.tangem.features.feed.model.feed.state.transformers.FeedListUMTransformer
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import kotlinx.collections.immutable.persistentHashMapOf
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -13,7 +16,9 @@ import kotlinx.coroutines.flow.update
|
|||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class FeedStateController @Inject constructor() {
|
||||
internal class FeedStateController @Inject constructor(
|
||||
private val feedFeatureToggle: FeedFeatureToggle,
|
||||
) {
|
||||
|
||||
private val mutableUiState: MutableStateFlow<FeedListUM> = MutableStateFlow(value = getInitialState())
|
||||
|
||||
|
|
@ -33,15 +38,11 @@ internal class FeedStateController @Inject constructor() {
|
|||
transformers.forEach { mutableUiState.update(function = it::transform) }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
mutableUiState.update { getInitialState() }
|
||||
}
|
||||
|
||||
private fun getInitialState(): FeedListUM {
|
||||
return FeedListUM(
|
||||
currentDate = "",
|
||||
feedListSearchBar = FeedListSearchBar(
|
||||
placeholderText = com.tangem.core.ui.extensions.TextReference.EMPTY,
|
||||
placeholderText = TextReference.EMPTY,
|
||||
onBarClick = {},
|
||||
),
|
||||
feedListCallbacks = FeedListCallbacks(
|
||||
|
|
@ -53,6 +54,7 @@ internal class FeedStateController @Inject constructor() {
|
|||
onSortTypeClick = {},
|
||||
onSliderScroll = {},
|
||||
onSliderEndReached = {},
|
||||
onOpenEarnPageClick = {},
|
||||
),
|
||||
news = NewsUM(
|
||||
content = persistentListOf(),
|
||||
|
|
@ -65,6 +67,11 @@ internal class FeedStateController @Inject constructor() {
|
|||
currentSortByType = SortByTypeUM.Trending,
|
||||
),
|
||||
globalState = GlobalFeedState.Loading,
|
||||
earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) {
|
||||
EarnListUM.Loading
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.feed.model.feed.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
|
||||
internal class UpdateEarnLoadingStateTransformer : FeedListUMTransformer {
|
||||
|
||||
override fun transform(prevState: FeedListUM): FeedListUM {
|
||||
return prevState.copy(earnListUM = EarnListUM.Loading)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.tangem.features.feed.model.feed.state.transformers
|
||||
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.domain.models.earn.EarnRewardType
|
||||
import com.tangem.domain.models.earn.EarnTokenWithCurrency
|
||||
import com.tangem.domain.models.earn.EarnTopToken
|
||||
import com.tangem.domain.models.earn.EarnType
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class UpdateEarnStateTransformer(
|
||||
private val isEarnEnabled: Boolean,
|
||||
private val onItemClick: (EarnTokenWithCurrency) -> Unit,
|
||||
private val onRetryClick: () -> Unit,
|
||||
private val earnResult: EarnTopToken?,
|
||||
) : FeedListUMTransformer {
|
||||
|
||||
private val earnTokenWithCurrencyConverter = EarnTokenWithCurrencyConverter(onItemClick = onItemClick)
|
||||
|
||||
override fun transform(prevState: FeedListUM): FeedListUM {
|
||||
if (!isEarnEnabled) return handleEmptyState(prevState)
|
||||
return when (earnResult) {
|
||||
null -> {
|
||||
handleEmptyState(prevState)
|
||||
}
|
||||
else -> {
|
||||
earnResult.fold(
|
||||
ifLeft = {
|
||||
handleErrorState(prevState)
|
||||
},
|
||||
ifRight = { earnTokensWithCurrency ->
|
||||
handleDataState(
|
||||
currentState = prevState,
|
||||
earnTokensWithCurrency = earnTokensWithCurrency,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEmptyState(currentState: FeedListUM): FeedListUM {
|
||||
return currentState.copy(earnListUM = null)
|
||||
}
|
||||
|
||||
private fun handleErrorState(currentState: FeedListUM): FeedListUM {
|
||||
return currentState.copy(earnListUM = EarnListUM.Error(onRetryClick))
|
||||
}
|
||||
|
||||
private fun handleDataState(
|
||||
currentState: FeedListUM,
|
||||
earnTokensWithCurrency: List<EarnTokenWithCurrency>,
|
||||
): FeedListUM {
|
||||
return currentState.copy(
|
||||
earnListUM = EarnListUM.Content(
|
||||
items = earnTokensWithCurrency
|
||||
.sortedWith(
|
||||
compareByDescending<EarnTokenWithCurrency> {
|
||||
it.earnToken.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO
|
||||
}.thenBy { it.earnToken.tokenName },
|
||||
)
|
||||
.map { earnToken ->
|
||||
earnTokenWithCurrencyConverter.convert(earnToken)
|
||||
}
|
||||
.toPersistentList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private class EarnTokenWithCurrencyConverter(
|
||||
private val onItemClick: (EarnTokenWithCurrency) -> Unit,
|
||||
) : Converter<EarnTokenWithCurrency, EarnListItemUM> {
|
||||
override fun convert(value: EarnTokenWithCurrency): EarnListItemUM {
|
||||
return EarnListItemUM(
|
||||
network = TextReference.Str(value.networkName),
|
||||
symbol = TextReference.Str(value.earnToken.tokenSymbol),
|
||||
tokenName = TextReference.Str(value.earnToken.tokenName),
|
||||
currencyIconState = CryptoCurrencyToIconStateConverter().convert(value.cryptoCurrency),
|
||||
earnValue = when (value.earnToken.rewardType) {
|
||||
EarnRewardType.APR -> TextReference.Res(
|
||||
id = R.string.staking_apr_earn_badge,
|
||||
formatArgs = wrappedList(convertPercent(value.earnToken.apy)),
|
||||
)
|
||||
EarnRewardType.APY -> TextReference.Res(
|
||||
id = R.string.yield_module_earn_badge,
|
||||
formatArgs = wrappedList(convertPercent(value.earnToken.apy)),
|
||||
)
|
||||
},
|
||||
earnType = when (value.earnToken.type) {
|
||||
EarnType.STAKING -> TextReference.Res(R.string.common_staking)
|
||||
EarnType.YIELD -> TextReference.Res(R.string.common_yield_mode)
|
||||
},
|
||||
onItemClick = { onItemClick(value) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertPercent(value: String): TextReference {
|
||||
val percent = BigDecimal(value).format { percent(withPercentSign = false) }
|
||||
return TextReference.Str(percent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +1,58 @@
|
|||
package com.tangem.features.feed.model.feed.state.transformers
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.models.earn.EarnTopToken
|
||||
import com.tangem.domain.models.news.TrendingNews
|
||||
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
||||
import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
import com.tangem.features.feed.ui.feed.state.GlobalFeedState
|
||||
import com.tangem.features.feed.ui.feed.state.NewsUMState
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class UpdateGlobalFeedStateTransformer(
|
||||
private val loadingStatesByOrder: Map<SortByTypeUM, Boolean>,
|
||||
private val errorStatesByOrder: Map<SortByTypeUM, Throwable?>,
|
||||
private val trendingNewsResult: TrendingNews,
|
||||
private val earnResult: EarnTopToken?,
|
||||
private val onRetryClicked: () -> Unit,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val feedFeatureToggle: FeedFeatureToggle,
|
||||
) : FeedListUMTransformer {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
override fun transform(prevState: FeedListUM): FeedListUM {
|
||||
val previousGlobalState = prevState.globalState
|
||||
val blockStates = buildList {
|
||||
add(getNewsState(prevState))
|
||||
add(getChartsState())
|
||||
if (feedFeatureToggle.isEarnBlockEnabled) {
|
||||
add(getEarnState(prevState))
|
||||
}
|
||||
}
|
||||
|
||||
val isNewsLoading = prevState.news.newsUMState == NewsUMState.LOADING
|
||||
val isNewsError = trendingNewsResult is TrendingNews.Error ||
|
||||
prevState.news.newsUMState == NewsUMState.ERROR
|
||||
val newGlobalState = when {
|
||||
blockStates.all { it == BlockState.ERROR } -> {
|
||||
if (prevState.globalState !is GlobalFeedState.Error) {
|
||||
sendErrorAnalytics()
|
||||
}
|
||||
GlobalFeedState.Error(onRetryClicked = onRetryClicked)
|
||||
}
|
||||
blockStates.any { it == BlockState.LOADING } -> GlobalFeedState.Loading
|
||||
else -> GlobalFeedState.Content
|
||||
}
|
||||
|
||||
return prevState.copy(globalState = newGlobalState)
|
||||
}
|
||||
|
||||
private fun areAllChartsError(): Boolean {
|
||||
val hasLoadingInCharts = loadingStatesByOrder.values.any { it }
|
||||
val hasErrorInMarketChart = errorStatesByOrder[SortByTypeUM.Rating] != null
|
||||
val hasErrorInMarketPulseChart = errorStatesByOrder
|
||||
.any { it.key != SortByTypeUM.Rating && it.value != null }
|
||||
|
||||
val areAllChartsLoading = loadingStatesByOrder.values.isNotEmpty() && loadingStatesByOrder.values.all { it }
|
||||
val areAllChartsError = when {
|
||||
hasErrorInMarketChart && hasErrorInMarketPulseChart -> true
|
||||
hasErrorInMarketPulseChart && hasLoadingInCharts -> true
|
||||
hasErrorInMarketChart && hasLoadingInCharts -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val newGlobalState = when {
|
||||
isNewsError && areAllChartsLoading -> GlobalFeedState.Loading
|
||||
isNewsError && areAllChartsError -> {
|
||||
if (previousGlobalState !is GlobalFeedState.Error) {
|
||||
sendErrorAnalytics()
|
||||
}
|
||||
GlobalFeedState.Error(
|
||||
onRetryClicked = onRetryClicked,
|
||||
)
|
||||
}
|
||||
isNewsLoading && areAllChartsError -> GlobalFeedState.Loading
|
||||
isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading
|
||||
else -> GlobalFeedState.Content
|
||||
}
|
||||
return prevState.copy(globalState = newGlobalState)
|
||||
return hasErrorInMarketChart && hasErrorInMarketPulseChart ||
|
||||
hasErrorInMarketPulseChart && hasLoadingInCharts ||
|
||||
hasErrorInMarketChart && hasLoadingInCharts
|
||||
}
|
||||
|
||||
private fun sendErrorAnalytics() {
|
||||
|
|
@ -59,4 +60,38 @@ internal class UpdateGlobalFeedStateTransformer(
|
|||
FeedAnalyticsEvent.AllWidgetsLoadError(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getNewsState(prevState: FeedListUM): BlockState {
|
||||
val isError = trendingNewsResult is TrendingNews.Error || prevState.news.newsUMState == NewsUMState.ERROR
|
||||
if (isError) return BlockState.ERROR
|
||||
|
||||
val isLoading = prevState.news.newsUMState == NewsUMState.LOADING && trendingNewsResult !is TrendingNews.Data
|
||||
if (isLoading) return BlockState.LOADING
|
||||
|
||||
return BlockState.CONTENT
|
||||
}
|
||||
|
||||
private fun getChartsState(): BlockState {
|
||||
if (areAllChartsError()) return BlockState.ERROR
|
||||
|
||||
val isLoading = loadingStatesByOrder.values.isNotEmpty() && loadingStatesByOrder.values.all { it }
|
||||
if (isLoading) return BlockState.LOADING
|
||||
|
||||
return BlockState.CONTENT
|
||||
}
|
||||
|
||||
private fun getEarnState(prevState: FeedListUM): BlockState {
|
||||
val isError = when (earnResult) {
|
||||
null -> false
|
||||
else -> earnResult.isLeft()
|
||||
}
|
||||
if (isError) return BlockState.ERROR
|
||||
|
||||
val isLoading = prevState.earnListUM is EarnListUM.Loading && earnResult == null
|
||||
if (isLoading) return BlockState.LOADING
|
||||
|
||||
return BlockState.CONTENT
|
||||
}
|
||||
|
||||
private enum class BlockState { LOADING, ERROR, CONTENT }
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
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.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
|
@ -83,6 +84,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val getNewsUseCase: GetNewsUseCase,
|
||||
private val shareManager: ShareManager,
|
||||
) : Model() {
|
||||
|
||||
private val quotesJob = JobHolder()
|
||||
|
|
@ -247,6 +249,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
onFirstVisible = {},
|
||||
onScroll = {},
|
||||
),
|
||||
onShareClick = ::onShareClick,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -662,6 +665,13 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onShareClick() {
|
||||
val tokenId = params.token.id.value
|
||||
val shareUrl = "$CRYPTOCURRENCIES_BASE_URL$tokenId"
|
||||
shareManager.shareText(shareUrl)
|
||||
analyticsEventHandler.send(analyticsEventBuilder.shareClicked())
|
||||
}
|
||||
|
||||
private fun onListedOnClick(exchangesCount: Int) {
|
||||
modelScope.launch {
|
||||
analyticsEventHandler.send(analyticsEventBuilder.exchangesScreenOpened())
|
||||
|
|
@ -725,5 +735,6 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
private companion object {
|
||||
const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L
|
||||
const val RELATED_NEWS_LIMIT = 10
|
||||
const val CRYPTOCURRENCIES_BASE_URL = "https://tangem.com/en/cryptocurrencies/"
|
||||
}
|
||||
}
|
||||
|
|
@ -65,6 +65,11 @@ internal class MarketDetailsAnalyticsEvent(
|
|||
"Provider" to provider,
|
||||
),
|
||||
)
|
||||
|
||||
fun shareClicked() = MarketDetailsAnalyticsEvent(
|
||||
event = "Button - Share",
|
||||
params = mapOf("Token" to token.symbol),
|
||||
)
|
||||
}
|
||||
|
||||
enum class IntervalType(val source: String) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.feed.ui.earn
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -9,12 +10,16 @@ import androidx.compose.foundation.lazy.LazyListScope
|
|||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -37,10 +42,16 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
|||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.feed.ui.earn.components.EarnFilterByNetworkBottomSheet
|
||||
import com.tangem.features.feed.ui.earn.components.EarnFilterByTypeBottomSheet
|
||||
import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder
|
||||
import com.tangem.features.feed.ui.earn.components.EarnListItem
|
||||
import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnUM
|
||||
|
|
@ -51,6 +62,10 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
|
|||
val background = LocalMainBottomSheetColor.current.value
|
||||
val density = LocalDensity.current
|
||||
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
EarnFilterByTypeBottomSheet(config = state.filterByTypeBottomSheet)
|
||||
EarnFilterByNetworkBottomSheet(config = state.filterByNetworkBottomSheet)
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -79,8 +94,8 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
|
|||
SpacerH(12.dp)
|
||||
BestOpportunitiesFilters(
|
||||
state = state.bestOpportunities,
|
||||
selectedNetworkFilter = state.selectedNetworkFilter,
|
||||
selectedTypeFilter = state.selectedTypeFilter,
|
||||
selectedNetworkFilterText = state.selectedNetworkFilterText,
|
||||
selectedTypeFilterText = state.selectedTypeFilterText,
|
||||
onNetworkFilterClick = state.onNetworkFilterClick,
|
||||
onTypeFilterClick = state.onTypeFilterClick,
|
||||
)
|
||||
|
|
@ -94,40 +109,42 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
|
|||
|
||||
@Composable
|
||||
private fun MostlyUsedContent(state: EarnListUM) {
|
||||
when (state) {
|
||||
is EarnListUM.Loading -> {
|
||||
MostlyUsedPlaceholder()
|
||||
}
|
||||
is EarnListUM.Content -> {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = 16.dp,
|
||||
vertical = 12.dp,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(
|
||||
items = state.items,
|
||||
key = { "${it.tokenName}-${it.network}" },
|
||||
) { item ->
|
||||
MostlyUsedCard(
|
||||
item = item,
|
||||
onClick = item.onItemClick,
|
||||
)
|
||||
}
|
||||
AnimatedContent(state) { st ->
|
||||
when (st) {
|
||||
is EarnListUM.Loading -> {
|
||||
MostlyUsedPlaceholder()
|
||||
}
|
||||
}
|
||||
is EarnListUM.Error -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
is EarnListUM.Content -> {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = 16.dp,
|
||||
vertical = 12.dp,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = state.onRetryClicked)
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(
|
||||
items = st.items,
|
||||
key = { "${it.tokenName}-${it.network}" },
|
||||
) { item ->
|
||||
MostlyUsedCard(
|
||||
item = item,
|
||||
onClick = item.onItemClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is EarnListUM.Error -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 12.dp,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = st.onRetryClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -184,40 +201,46 @@ private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier:
|
|||
|
||||
@Composable
|
||||
private fun BestOpportunitiesFilters(
|
||||
state: EarnListUM,
|
||||
selectedNetworkFilter: EarnFilterUM?,
|
||||
selectedTypeFilter: EarnFilterUM?,
|
||||
state: EarnBestOpportunitiesUM,
|
||||
selectedNetworkFilterText: TextReference,
|
||||
selectedTypeFilterText: TextReference,
|
||||
onNetworkFilterClick: () -> Unit,
|
||||
onTypeFilterClick: () -> Unit,
|
||||
) {
|
||||
when (state) {
|
||||
is EarnListUM.Loading -> {
|
||||
FilterButtonsShimmer()
|
||||
}
|
||||
is EarnListUM.Content -> {
|
||||
FilterButtons(
|
||||
selectedNetworkFilter = selectedNetworkFilter,
|
||||
selectedTypeFilter = selectedTypeFilter,
|
||||
isEnabled = true,
|
||||
onNetworkFilterClick = onNetworkFilterClick,
|
||||
onTypeFilterClick = onTypeFilterClick,
|
||||
)
|
||||
}
|
||||
is EarnListUM.Error -> {
|
||||
FilterButtons(
|
||||
selectedNetworkFilter = selectedNetworkFilter,
|
||||
selectedTypeFilter = selectedTypeFilter,
|
||||
isEnabled = false,
|
||||
onNetworkFilterClick = onNetworkFilterClick,
|
||||
onTypeFilterClick = onTypeFilterClick,
|
||||
)
|
||||
AnimatedContent(state) { st ->
|
||||
when (st) {
|
||||
is EarnBestOpportunitiesUM.Loading -> {
|
||||
FilterButtonsShimmer()
|
||||
}
|
||||
is EarnBestOpportunitiesUM.EmptyFiltered,
|
||||
is EarnBestOpportunitiesUM.Content,
|
||||
-> {
|
||||
FilterButtons(
|
||||
selectedNetworkFilterText = selectedNetworkFilterText,
|
||||
selectedTypeFilterText = selectedTypeFilterText,
|
||||
isEnabled = true,
|
||||
onNetworkFilterClick = onNetworkFilterClick,
|
||||
onTypeFilterClick = onTypeFilterClick,
|
||||
)
|
||||
}
|
||||
is EarnBestOpportunitiesUM.Empty,
|
||||
is EarnBestOpportunitiesUM.Error,
|
||||
-> {
|
||||
FilterButtons(
|
||||
selectedNetworkFilterText = selectedNetworkFilterText,
|
||||
selectedTypeFilterText = selectedTypeFilterText,
|
||||
isEnabled = false,
|
||||
onNetworkFilterClick = onNetworkFilterClick,
|
||||
onTypeFilterClick = onTypeFilterClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) {
|
||||
private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM) {
|
||||
when (state) {
|
||||
is EarnListUM.Loading -> {
|
||||
is EarnBestOpportunitiesUM.Loading -> {
|
||||
val lastIndex = PLACEHOLDER_ITEMS_COUNT - 1
|
||||
items(
|
||||
count = PLACEHOLDER_ITEMS_COUNT,
|
||||
|
|
@ -233,7 +256,19 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) {
|
|||
)
|
||||
}
|
||||
}
|
||||
is EarnListUM.Content -> {
|
||||
is EarnBestOpportunitiesUM.Empty -> {
|
||||
item(key = "best_opportunities_empty") {
|
||||
SpacerH(12.dp)
|
||||
BestOpportunitiesEmpty()
|
||||
}
|
||||
}
|
||||
is EarnBestOpportunitiesUM.EmptyFiltered -> {
|
||||
item(key = "best_opportunities_empty_filtered") {
|
||||
SpacerH(12.dp)
|
||||
BestOpportunitiesEmptyFiltered(onClearFilterClick = state.onClearFilterClick)
|
||||
}
|
||||
}
|
||||
is EarnBestOpportunitiesUM.Content -> {
|
||||
if (state.items.isNotEmpty()) {
|
||||
val lastIndex = state.items.lastIndex
|
||||
itemsIndexed(
|
||||
|
|
@ -252,7 +287,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) {
|
|||
}
|
||||
}
|
||||
}
|
||||
is EarnListUM.Error -> {
|
||||
is EarnBestOpportunitiesUM.Error -> {
|
||||
item(key = "best_opportunities_error") {
|
||||
SpacerH(12.dp)
|
||||
Box(
|
||||
|
|
@ -275,8 +310,8 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) {
|
|||
|
||||
@Composable
|
||||
private fun FilterButtons(
|
||||
selectedNetworkFilter: EarnFilterUM?,
|
||||
selectedTypeFilter: EarnFilterUM?,
|
||||
selectedNetworkFilterText: TextReference,
|
||||
selectedTypeFilterText: TextReference,
|
||||
isEnabled: Boolean,
|
||||
onNetworkFilterClick: () -> Unit,
|
||||
onTypeFilterClick: () -> Unit,
|
||||
|
|
@ -287,8 +322,7 @@ private fun FilterButtons(
|
|||
) {
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = selectedNetworkFilter?.name
|
||||
?: resourceReference(R.string.earn_filter_all_networks),
|
||||
text = selectedNetworkFilterText,
|
||||
onClick = onNetworkFilterClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
isEnabled = isEnabled,
|
||||
|
|
@ -299,8 +333,7 @@ private fun FilterButtons(
|
|||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = selectedTypeFilter?.name
|
||||
?: resourceReference(R.string.earn_filter_all_types),
|
||||
text = selectedTypeFilterText,
|
||||
onClick = onTypeFilterClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
|
||||
isEnabled = isEnabled,
|
||||
|
|
@ -326,6 +359,66 @@ private fun FilterButtonsShimmer(modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BestOpportunitiesEmpty(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(vertical = 32.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
painter = painterResource(R.drawable.ic_empty_64),
|
||||
contentDescription = null,
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
SpacerH(24.dp)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 32.dp),
|
||||
text = stringResourceSafe(R.string.earn_empty),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BestOpportunitiesEmptyFiltered(onClearFilterClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(vertical = 32.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.earn_no_results),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.earn_clear_filter),
|
||||
onClick = onClearFilterClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(title: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
|
|
@ -348,7 +441,7 @@ private fun EarnContentPreview() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
state = EarnUM(
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Content(
|
||||
items = persistentListOf(
|
||||
previewEarnListItemUM(),
|
||||
|
|
@ -359,7 +452,7 @@ private fun EarnContentPreview() {
|
|||
),
|
||||
),
|
||||
),
|
||||
bestOpportunities = EarnListUM.Content(
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Content(
|
||||
items = persistentListOf(
|
||||
previewEarnListItemUM(
|
||||
tokenName = "Cosmos Hub",
|
||||
|
|
@ -373,13 +466,6 @@ private fun EarnContentPreview() {
|
|||
),
|
||||
),
|
||||
),
|
||||
selectedNetworkFilter = null,
|
||||
selectedTypeFilter = null,
|
||||
networkFilters = persistentListOf(),
|
||||
typeFilters = persistentListOf(),
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -396,16 +482,9 @@ private fun EarnContentLoadingPreview() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
state = EarnUM(
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Loading,
|
||||
bestOpportunities = EarnListUM.Loading,
|
||||
selectedNetworkFilter = null,
|
||||
selectedTypeFilter = null,
|
||||
networkFilters = persistentListOf(),
|
||||
typeFilters = persistentListOf(),
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Loading,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -422,7 +501,7 @@ private fun EarnContentErrorPreview() {
|
|||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
state = EarnUM(
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Content(
|
||||
items = persistentListOf(
|
||||
previewEarnListItemUM(),
|
||||
|
|
@ -433,14 +512,35 @@ private fun EarnContentErrorPreview() {
|
|||
),
|
||||
),
|
||||
),
|
||||
bestOpportunities = EarnListUM.Error(onRetryClicked = {}),
|
||||
selectedNetworkFilter = null,
|
||||
selectedTypeFilter = null,
|
||||
networkFilters = persistentListOf(),
|
||||
typeFilters = persistentListOf(),
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Error(onRetryClicked = {}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun EarnContentEmptyPreview() {
|
||||
TangemThemePreview {
|
||||
val background = TangemTheme.colors.background.tertiary
|
||||
CompositionLocalProvider(
|
||||
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
|
||||
) {
|
||||
EarnContent(
|
||||
state = previewEarnUM(
|
||||
mostlyUsed = EarnListUM.Content(
|
||||
items = persistentListOf(
|
||||
previewEarnListItemUM(),
|
||||
previewEarnListItemUM(
|
||||
tokenName = "Cosmos",
|
||||
symbol = "ATOM",
|
||||
network = "Cosmos",
|
||||
),
|
||||
),
|
||||
),
|
||||
bestOpportunities = EarnBestOpportunitiesUM.Empty,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -469,4 +569,22 @@ private fun previewEarnListItemUM(
|
|||
onItemClick = {},
|
||||
)
|
||||
|
||||
private fun previewEarnUM(
|
||||
mostlyUsed: EarnListUM = EarnListUM.Loading,
|
||||
bestOpportunities: EarnBestOpportunitiesUM = EarnBestOpportunitiesUM.Loading,
|
||||
): EarnUM = EarnUM(
|
||||
mostlyUsed = mostlyUsed,
|
||||
bestOpportunities = bestOpportunities,
|
||||
selectedTypeFilter = EarnFilterTypeUM.All,
|
||||
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(
|
||||
text = resourceReference(R.string.earn_filter_all_networks),
|
||||
isSelected = true,
|
||||
),
|
||||
filterByTypeBottomSheet = TangemBottomSheetConfig.Empty,
|
||||
filterByNetworkBottomSheet = TangemBottomSheetConfig.Empty,
|
||||
onBackClick = {},
|
||||
onNetworkFilterClick = {},
|
||||
onTypeFilterClick = {},
|
||||
)
|
||||
|
||||
private const val PLACEHOLDER_ITEMS_COUNT = 8
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
package com.tangem.features.feed.ui.earn.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.SpacerH
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.inputrow.InputRowChecked
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.components.rows.RowContentContainer
|
||||
import com.tangem.core.ui.components.rows.RowText
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
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.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun EarnFilterByNetworkBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<EarnFilterByNetworkBottomSheetContentUM>(
|
||||
config = config,
|
||||
titleText = resourceReference(R.string.earn_filter_by),
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
content = { Content(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(content: EarnFilterByNetworkBottomSheetContentUM) {
|
||||
val allMyNetworks = remember(content) {
|
||||
content.networks.filterIsInstance<EarnFilterNetworkUM.AllNetworks>() +
|
||||
content.networks.filterIsInstance<EarnFilterNetworkUM.MyNetworks>()
|
||||
}
|
||||
val specificNetworks = remember(content) { content.networks.filterIsInstance<EarnFilterNetworkUM.Network>() }
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
allMyNetworksList(
|
||||
allMyNetworks = allMyNetworks,
|
||||
onOptionClicked = content.onOptionClick,
|
||||
)
|
||||
|
||||
if (specificNetworks.isNotEmpty()) {
|
||||
networksHeader()
|
||||
|
||||
specificNetworksList(
|
||||
specificNetworks = specificNetworks,
|
||||
onOptionClicked = content.onOptionClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.allMyNetworksList(
|
||||
allMyNetworks: List<EarnFilterNetworkUM>,
|
||||
onOptionClicked: (EarnFilterNetworkUM) -> Unit,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = allMyNetworks,
|
||||
key = { _, item ->
|
||||
when (item) {
|
||||
is EarnFilterNetworkUM.AllNetworks -> "all_networks"
|
||||
is EarnFilterNetworkUM.MyNetworks -> "my_networks"
|
||||
is EarnFilterNetworkUM.Network -> item.id
|
||||
}
|
||||
},
|
||||
) { index, item ->
|
||||
DividerContainer(
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = allMyNetworks.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable { onOptionClicked(item) },
|
||||
showDivider = index != allMyNetworks.lastIndex,
|
||||
) {
|
||||
InputRowChecked(
|
||||
text = item.text,
|
||||
checked = item.isSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.networksHeader() {
|
||||
item(key = "networks_header") {
|
||||
SpacerH(TangemTheme.dimens.spacing16)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = 16.dp,
|
||||
topEnd = 16.dp,
|
||||
),
|
||||
)
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 12.dp,
|
||||
bottom = 4.dp,
|
||||
),
|
||||
text = stringResourceSafe(id = R.string.earn_filter_networks),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.specificNetworksList(
|
||||
specificNetworks: List<EarnFilterNetworkUM.Network>,
|
||||
onOptionClicked: (EarnFilterNetworkUM) -> Unit,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = specificNetworks,
|
||||
key = { _, item -> item.id },
|
||||
) { index, item ->
|
||||
DividerContainer(
|
||||
modifier = Modifier
|
||||
.height(52.dp)
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index + 1,
|
||||
lastIndex = specificNetworks.lastIndex + 1,
|
||||
addDefaultPadding = false,
|
||||
)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable { onOptionClicked(item) },
|
||||
showDivider = false,
|
||||
) {
|
||||
RowContentContainer(
|
||||
modifier = Modifier
|
||||
.heightIn(52.dp)
|
||||
.padding(horizontal = 12.dp),
|
||||
icon = {
|
||||
CoinIcon(
|
||||
modifier = Modifier.size(22.dp),
|
||||
url = item.iconUrl,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
RowText(
|
||||
mainText = item.text.resolveReference(),
|
||||
secondText = item.symbol.resolveReference(),
|
||||
accentMainText = true,
|
||||
accentSecondText = false,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
if (item.isSelected) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_check_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, heightDp = 800)
|
||||
@Preview(widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview(
|
||||
alwaysShowBottomSheets = true,
|
||||
) {
|
||||
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
EarnFilterByNetworkBottomSheet(
|
||||
TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
content = EarnFilterByNetworkBottomSheetContentUM(
|
||||
selectedNetwork = EarnFilterNetworkUM.AllNetworks(
|
||||
text = resourceReference(R.string.earn_filter_all_networks),
|
||||
isSelected = true,
|
||||
),
|
||||
networks = persistentListOf(
|
||||
EarnFilterNetworkUM.AllNetworks(
|
||||
text = resourceReference(R.string.earn_filter_all_networks),
|
||||
isSelected = true,
|
||||
),
|
||||
EarnFilterNetworkUM.MyNetworks(
|
||||
text = stringReference("My network"),
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.Network(
|
||||
id = "ethereum",
|
||||
text = stringReference("Ethereum"),
|
||||
symbol = stringReference("ETH"),
|
||||
iconUrl = null,
|
||||
isSelected = false,
|
||||
),
|
||||
EarnFilterNetworkUM.Network(
|
||||
id = "polygon",
|
||||
text = stringReference("Polygon"),
|
||||
symbol = stringReference("MATIC"),
|
||||
iconUrl = null,
|
||||
isSelected = false,
|
||||
),
|
||||
),
|
||||
onOptionClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.features.feed.ui.earn.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.inputrow.InputRowChecked
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
|
||||
|
||||
@Composable
|
||||
internal fun EarnFilterByTypeBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<EarnFilterByTypeBottomSheetContentUM>(
|
||||
config = config,
|
||||
titleText = resourceReference(R.string.earn_filter_by),
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
content = { Content(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(content: EarnFilterByTypeBottomSheetContentUM) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
EarnFilterTypeUM.entries.forEachIndexed { index, type ->
|
||||
DividerContainer(
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = EarnFilterTypeUM.entries.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable { content.onOptionClick(type) },
|
||||
showDivider = index != EarnFilterTypeUM.entries.lastIndex,
|
||||
) {
|
||||
InputRowChecked(
|
||||
text = type.text,
|
||||
checked = type == content.selectedOption,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, heightDp = 640)
|
||||
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview(
|
||||
alwaysShowBottomSheets = true,
|
||||
) {
|
||||
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
|
||||
EarnFilterByTypeBottomSheet(
|
||||
TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {},
|
||||
content = EarnFilterByTypeBottomSheetContentUM(
|
||||
selectedOption = EarnFilterTypeUM.All,
|
||||
onOptionClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,11 +17,11 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
internal fun EarnListPlaceholder(modifier: Modifier = Modifier) {
|
||||
internal fun EarnListPlaceholder(modifier: Modifier = Modifier, placeholderCount: Int = PLACEHOLDER_ITEMS_COUNT) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
repeat(PLACEHOLDER_ITEMS_COUNT) {
|
||||
repeat(placeholderCount) {
|
||||
EarnItemPlaceholder()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class EarnFilterByNetworkBottomSheetContentUM(
|
||||
val selectedNetwork: EarnFilterNetworkUM,
|
||||
val networks: ImmutableList<EarnFilterNetworkUM>,
|
||||
val onOptionClick: (EarnFilterNetworkUM) -> Unit,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
||||
internal data class EarnFilterByTypeBottomSheetContentUM(
|
||||
val selectedOption: EarnFilterTypeUM,
|
||||
val onOptionClick: (EarnFilterTypeUM) -> Unit,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class EarnFilterNetworkUM {
|
||||
|
||||
abstract val text: TextReference
|
||||
abstract val isSelected: Boolean
|
||||
|
||||
data class AllNetworks(
|
||||
override val text: TextReference,
|
||||
override val isSelected: Boolean,
|
||||
) : EarnFilterNetworkUM()
|
||||
|
||||
data class MyNetworks(
|
||||
override val text: TextReference,
|
||||
override val isSelected: Boolean,
|
||||
) : EarnFilterNetworkUM()
|
||||
|
||||
data class Network(
|
||||
val id: String,
|
||||
override val text: TextReference,
|
||||
val symbol: TextReference,
|
||||
val iconUrl: String?,
|
||||
override val isSelected: Boolean,
|
||||
) : EarnFilterNetworkUM()
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
||||
internal enum class EarnFilterTypeUM(val text: TextReference) {
|
||||
All(resourceReference(R.string.earn_filter_all_types)),
|
||||
Staking(resourceReference(R.string.common_staking)),
|
||||
YieldMode(resourceReference(R.string.markets_sort_by_yield_mode_title)),
|
||||
}
|
||||
|
|
@ -12,6 +12,15 @@ internal sealed interface EarnListUM {
|
|||
data class Error(val onRetryClicked: () -> Unit) : EarnListUM
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal sealed interface EarnBestOpportunitiesUM {
|
||||
data object Loading : EarnBestOpportunitiesUM
|
||||
data object Empty : EarnBestOpportunitiesUM
|
||||
data class EmptyFiltered(val onClearFilterClick: () -> Unit) : EarnBestOpportunitiesUM
|
||||
data class Content(val items: ImmutableList<EarnListItemUM>) : EarnBestOpportunitiesUM
|
||||
data class Error(val onRetryClicked: () -> Unit) : EarnBestOpportunitiesUM
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal data class EarnListItemUM(
|
||||
val network: TextReference,
|
||||
|
|
|
|||
|
|
@ -1,26 +1,25 @@
|
|||
package com.tangem.features.feed.ui.earn.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal data class EarnUM(
|
||||
val mostlyUsed: EarnListUM,
|
||||
val bestOpportunities: EarnListUM,
|
||||
val selectedNetworkFilter: EarnFilterUM?,
|
||||
val selectedTypeFilter: EarnFilterUM?,
|
||||
val networkFilters: ImmutableList<EarnFilterUM>,
|
||||
val typeFilters: ImmutableList<EarnFilterUM>,
|
||||
val bestOpportunities: EarnBestOpportunitiesUM,
|
||||
val selectedTypeFilter: EarnFilterTypeUM,
|
||||
val selectedNetworkFilter: EarnFilterNetworkUM,
|
||||
val filterByTypeBottomSheet: TangemBottomSheetConfig,
|
||||
val filterByNetworkBottomSheet: TangemBottomSheetConfig,
|
||||
val onBackClick: () -> Unit,
|
||||
val onNetworkFilterClick: () -> Unit,
|
||||
val onTypeFilterClick: () -> Unit,
|
||||
)
|
||||
) {
|
||||
|
||||
@Immutable
|
||||
internal data class EarnFilterUM(
|
||||
val id: String,
|
||||
val name: TextReference,
|
||||
val isSelected: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
val selectedNetworkFilterText: TextReference
|
||||
get() = selectedNetworkFilter.text
|
||||
|
||||
val selectedTypeFilterText: TextReference
|
||||
get() = selectedTypeFilter.text
|
||||
}
|
||||
|
|
@ -4,57 +4,38 @@ import androidx.compose.animation.AnimatedContent
|
|||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.common.ui.news.ShowMoreArticlesCard
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.BaseSearchBarTestTags.SEARCH_BAR
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.feed.components.*
|
||||
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
import com.tangem.features.feed.ui.feed.state.GlobalFeedState
|
||||
|
||||
@Composable
|
||||
internal fun FeedListHeader(
|
||||
|
|
@ -69,7 +50,8 @@ internal fun FeedListHeader(
|
|||
modifier = modifier
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 8.dp),
|
||||
.padding(bottom = 8.dp)
|
||||
.testTag(SEARCH_BAR),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +146,11 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
|||
trendingArticle = state.trendingArticle,
|
||||
)
|
||||
|
||||
EarnBlock(
|
||||
onSeeAllClick = state.feedListCallbacks.onOpenEarnPageClick,
|
||||
earnListUM = state.earnListUM,
|
||||
)
|
||||
|
||||
MarketPulseBlock(
|
||||
marketChartConfig = state.marketChartConfig,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
|
|
@ -171,400 +158,6 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedListCallbacks) {
|
||||
AnimatedContent(
|
||||
targetState = marketChart,
|
||||
label = "MarketBlockChartAnimation",
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
) { currentChart ->
|
||||
when (currentChart) {
|
||||
is MarketChartUM.Content,
|
||||
MarketChartUM.Loading,
|
||||
is MarketChartUM.LoadingError,
|
||||
-> {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
marketChart = currentChart,
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
val onSeeAllClick by rememberUpdatedState {
|
||||
feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType)
|
||||
}
|
||||
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_pulse_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { onSeeAllClick() },
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
items(
|
||||
items = marketChartConfig.getFilterPreset(),
|
||||
key = SortByTypeUM::name,
|
||||
) { sortByTypeUM ->
|
||||
FilterChip(
|
||||
sortByTypeUM = sortByTypeUM,
|
||||
isSelected = sortByTypeUM == marketChartConfig.currentSortByType,
|
||||
onClick = { feedListCallbacks.onSortTypeClick(sortByTypeUM) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
AnimatedContent(
|
||||
targetState = marketChartConfig.currentSortByType,
|
||||
label = "MarketPulseChartAnimation",
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
) { currentSortType ->
|
||||
marketChartConfig.marketCharts[currentSortType]?.let { chart ->
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
marketChart = chart,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
AnimatedContent(news.newsUMState) { newsUMState ->
|
||||
when (newsUMState) {
|
||||
NewsUMState.LOADING -> NewsLoadingBlock()
|
||||
NewsUMState.CONTENT -> {
|
||||
if (news.content.isNotEmpty()) {
|
||||
NewsContentBlock(
|
||||
feedListCallbacks = feedListCallbacks,
|
||||
news = news,
|
||||
trendingArticle = trendingArticle,
|
||||
)
|
||||
}
|
||||
}
|
||||
NewsUMState.ERROR -> NewsErrorBlock(onRetryClick = news.onRetryClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
val listState = rememberLazyListState()
|
||||
val articlesReadStatus = remember(news.content) {
|
||||
news.content.map { it.isViewed }
|
||||
}
|
||||
LaunchedEffect(articlesReadStatus) {
|
||||
listState.requestScrollToItem(0)
|
||||
}
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
SpacerW(4.dp)
|
||||
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_stars_20),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW(2.dp)
|
||||
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
SpanStyle().copy(
|
||||
brush = Brush.linearGradient(
|
||||
GRADIENT_START to LinearGradientFirstPart,
|
||||
GRADIENT_END to LinearGradientSecondPart,
|
||||
),
|
||||
),
|
||||
) {
|
||||
append(stringResourceSafe(R.string.feed_tangem_ai))
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) },
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
|
||||
if (trendingArticle != null) {
|
||||
Column {
|
||||
ArticleCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
articleConfigUM = trendingArticle,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(trendingArticle.id) },
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
}
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = news.content,
|
||||
key = { _, article -> article.id },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = feedListCallbacks.onSliderScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
modifier = articleModifier
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.width(216.dp)
|
||||
.heightIn(min = 164.dp)
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = feedListCallbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = { feedListCallbacks.onOpenAllNews(true) },
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
title()
|
||||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.common_see_all),
|
||||
onClick = onSeeAllClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Charts(
|
||||
marketChart: MarketChartUM,
|
||||
onItemClick: (MarketsListItemUM) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
when (marketChart) {
|
||||
MarketChartUM.Loading -> {
|
||||
repeat(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
is MarketChartUM.LoadingError -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 35.dp, horizontal = 10.dp),
|
||||
) {
|
||||
UnableToLoadData(
|
||||
onRetryClick = marketChart.onRetryClicked,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
is MarketChartUM.Content -> {
|
||||
marketChart.items.fastForEach { chart ->
|
||||
MarketsListItem(
|
||||
model = chart,
|
||||
onClick = { onItemClick(chart) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FilterChip(sortByTypeUM: SortByTypeUM, isSelected: Boolean, onClick: () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(shape = RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
color = if (isSelected) {
|
||||
TangemTheme.colors.button.primary
|
||||
} else {
|
||||
TangemTheme.colors.button.secondary
|
||||
},
|
||||
)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = sortByTypeUM.text.resolveReference(),
|
||||
style = TangemTheme.typography.button,
|
||||
color = if (isSelected) {
|
||||
TangemTheme.colors.text.primary2
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeedListGlobalError(onRetryClick: () -> Unit, currentDate: String, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
Column(modifier) {
|
||||
DateBlock(currentDate)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = onRetryClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsErrorBlock(onRetryClick: () -> Unit) {
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = {},
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
BlockCard(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
UnableToLoadData(
|
||||
onRetryClick = onRetryClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 35.dp, horizontal = 10.dp),
|
||||
)
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.DateBlock(currentDate: String) {
|
||||
SpacerH(20.dp)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
text = stringResourceSafe(R.string.feed_market_and_news),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
text = currentDate,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
private const val DEFAULT_CHART_SIZE_IN_MARKET = 5
|
||||
private const val FOURTH_ITEM_INDEX = 3
|
||||
private const val GRADIENT_START = 0f
|
||||
private const val GRADIENT_END = 0.5f
|
||||
private val LinearGradientFirstPart = Color(0xFF635EEC)
|
||||
private val LinearGradientSecondPart = Color(0xFFE05AED)
|
||||
|
||||
@Preview(showBackground = true, heightDp = 1500)
|
||||
@Composable
|
||||
private fun FeedListPreview() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Composable
|
||||
internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title: @Composable () -> Unit) {
|
||||
AnimatedContent(isLoading) { animatedState ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (animatedState) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 104.dp, height = 18.dp))
|
||||
} else {
|
||||
title()
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.common_see_all),
|
||||
onClick = onSeeAllClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
|
||||
@Composable
|
||||
internal fun ChartsFilterChip(sortByTypeUM: SortByTypeUM, isSelected: Boolean, onClick: () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(shape = RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
color = if (isSelected) {
|
||||
TangemTheme.colors.button.primary
|
||||
} else {
|
||||
TangemTheme.colors.button.secondary
|
||||
},
|
||||
)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = sortByTypeUM.text.resolveReference(),
|
||||
style = TangemTheme.typography.button,
|
||||
color = if (isSelected) {
|
||||
TangemTheme.colors.text.primary2
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun DateBlock(currentDate: String) {
|
||||
SpacerH(20.dp)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
text = stringResourceSafe(R.string.feed_market_and_news),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
text = currentDate,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.earn.components.EarnListItem
|
||||
import com.tangem.features.feed.ui.earn.components.EarnListPlaceholder
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modifier: Modifier = Modifier) {
|
||||
if (earnListUM == null) return
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_earn_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = onSeeAllClick,
|
||||
isLoading = earnListUM is EarnListUM.Loading,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
BlockCard(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
AnimatedContent(targetState = earnListUM) { earnListState ->
|
||||
when (earnListState) {
|
||||
is EarnListUM.Content -> EarnContentBlock(items = earnListState.items)
|
||||
is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListState.onRetryClicked)
|
||||
EarnListUM.Loading -> EarnListPlaceholder(placeholderCount = PLACEHOLDER_ITEM_COUNT)
|
||||
}
|
||||
}
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EarnErrorBlock(onRetryClick: () -> Unit) {
|
||||
UnableToLoadData(
|
||||
onRetryClick = onRetryClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 35.dp, horizontal = 10.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EarnContentBlock(items: ImmutableList<EarnListItemUM>) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
items.fastForEach { item ->
|
||||
EarnListItem(item = item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val PLACEHOLDER_ITEM_COUNT = 5
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
|
||||
@Composable
|
||||
internal fun FeedListGlobalError(onRetryClick: () -> Unit, currentDate: String, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
Column(modifier) {
|
||||
DateBlock(currentDate)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = onRetryClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.feed.ui.feed
|
||||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
|
|
@ -14,6 +14,8 @@ import com.tangem.common.ui.news.TrendingLoadingArticle
|
|||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
|
|
@ -94,7 +96,10 @@ internal fun NewsLoadingBlock() {
|
|||
|
||||
@Composable
|
||||
private fun ChartsLoading(modifier: Modifier = Modifier) {
|
||||
BlockCard(modifier) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListCallbacks
|
||||
import com.tangem.features.feed.ui.feed.state.MarketChartConfig
|
||||
import com.tangem.features.feed.ui.feed.state.MarketChartUM
|
||||
|
||||
@Composable
|
||||
internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedListCallbacks) {
|
||||
AnimatedContent(
|
||||
targetState = marketChart,
|
||||
label = "MarketBlockChartAnimation",
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
) { currentChart ->
|
||||
when (currentChart) {
|
||||
is MarketChartUM.Content,
|
||||
MarketChartUM.Loading,
|
||||
is MarketChartUM.LoadingError,
|
||||
-> {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) },
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
marketChart = currentChart,
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
val onSeeAllClick by rememberUpdatedState {
|
||||
feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType)
|
||||
}
|
||||
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_pulse_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { onSeeAllClick() },
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
items(
|
||||
items = marketChartConfig.getFilterPreset(),
|
||||
key = SortByTypeUM::name,
|
||||
) { sortByTypeUM ->
|
||||
ChartsFilterChip(
|
||||
sortByTypeUM = sortByTypeUM,
|
||||
isSelected = sortByTypeUM == marketChartConfig.currentSortByType,
|
||||
onClick = { feedListCallbacks.onSortTypeClick(sortByTypeUM) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
AnimatedContent(
|
||||
targetState = marketChartConfig.currentSortByType,
|
||||
label = "MarketPulseChartAnimation",
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
) { currentSortType ->
|
||||
marketChartConfig.marketCharts[currentSortType]?.let { chart ->
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
marketChart = chart,
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Charts(
|
||||
marketChart: MarketChartUM,
|
||||
onItemClick: (MarketsListItemUM) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
when (marketChart) {
|
||||
MarketChartUM.Loading -> {
|
||||
repeat(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
is MarketChartUM.LoadingError -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 35.dp, horizontal = 10.dp),
|
||||
) {
|
||||
UnableToLoadData(
|
||||
onRetryClick = marketChart.onRetryClicked,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
is MarketChartUM.Content -> {
|
||||
marketChart.items.fastForEach { chart ->
|
||||
MarketsListItem(
|
||||
model = chart,
|
||||
onClick = { onItemClick(chart) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_CHART_SIZE_IN_MARKET = 5
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.tangem.features.feed.ui.feed.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.common.ui.news.ShowMoreArticlesCard
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListCallbacks
|
||||
import com.tangem.features.feed.ui.feed.state.NewsUM
|
||||
import com.tangem.features.feed.ui.feed.state.NewsUMState
|
||||
|
||||
private const val FOURTH_ITEM_INDEX = 3
|
||||
private const val GRADIENT_START = 0f
|
||||
private const val GRADIENT_END = 0.5f
|
||||
private val LinearGradientFirstPart = Color(0xFF635EEC)
|
||||
private val LinearGradientSecondPart = Color(0xFFE05AED)
|
||||
|
||||
@Composable
|
||||
internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
AnimatedContent(news.newsUMState) { newsUMState ->
|
||||
when (newsUMState) {
|
||||
NewsUMState.LOADING -> NewsLoadingBlock()
|
||||
NewsUMState.CONTENT -> {
|
||||
if (news.content.isNotEmpty()) {
|
||||
NewsContentBlock(
|
||||
feedListCallbacks = feedListCallbacks,
|
||||
news = news,
|
||||
trendingArticle = trendingArticle,
|
||||
)
|
||||
}
|
||||
}
|
||||
NewsUMState.ERROR -> NewsErrorBlock(onRetryClick = news.onRetryClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
val listState = rememberLazyListState()
|
||||
val articlesReadStatus = remember(news.content) {
|
||||
news.content.map { it.isViewed }
|
||||
}
|
||||
LaunchedEffect(articlesReadStatus) {
|
||||
listState.requestScrollToItem(0)
|
||||
}
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
SpacerW(4.dp)
|
||||
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_stars_20),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW(2.dp)
|
||||
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
SpanStyle().copy(
|
||||
brush = Brush.linearGradient(
|
||||
GRADIENT_START to LinearGradientFirstPart,
|
||||
GRADIENT_END to LinearGradientSecondPart,
|
||||
),
|
||||
),
|
||||
) {
|
||||
append(stringResourceSafe(R.string.feed_tangem_ai))
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) },
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
|
||||
if (trendingArticle != null) {
|
||||
Column {
|
||||
ArticleCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
articleConfigUM = trendingArticle,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(trendingArticle.id) },
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
}
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = news.content,
|
||||
key = { _, article -> article.id },
|
||||
contentType = { _, _ -> "article" },
|
||||
) { index, article ->
|
||||
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = feedListCallbacks.onSliderScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
modifier = articleModifier
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.width(216.dp)
|
||||
.heightIn(min = 164.dp)
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = feedListCallbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = { feedListCallbacks.onOpenAllNews(true) },
|
||||
)
|
||||
}
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsErrorBlock(onRetryClick: () -> Unit) {
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = {},
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
BlockCard(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
UnableToLoadData(
|
||||
onRetryClick = onRetryClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 35.dp, horizontal = 10.dp),
|
||||
)
|
||||
}
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,13 +3,18 @@ package com.tangem.features.feed.ui.feed.preview
|
|||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
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.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import kotlinx.collections.immutable.*
|
||||
|
||||
|
|
@ -34,6 +39,7 @@ internal object FeedListPreviewDataProvider {
|
|||
onSortTypeClick = {},
|
||||
onSliderScroll = {},
|
||||
onSliderEndReached = {},
|
||||
onOpenEarnPageClick = {},
|
||||
),
|
||||
news = NewsUM(
|
||||
content = articles.filter { it.isTrending.not() }.toImmutableList(),
|
||||
|
|
@ -45,6 +51,9 @@ internal object FeedListPreviewDataProvider {
|
|||
marketCharts = createMarketCharts(marketItems, includeErrorState = false),
|
||||
currentSortByType = SortByTypeUM.TopGainers,
|
||||
),
|
||||
earnListUM = EarnListUM.Content(
|
||||
items = createEarnListItemsUM(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -250,4 +259,25 @@ internal object FeedListPreviewDataProvider {
|
|||
updateTimestamp = 0,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createEarnListItemsUM(): ImmutableList<EarnListItemUM> {
|
||||
return List(5) {
|
||||
EarnListItemUM(
|
||||
network = stringReference("Ethereum"),
|
||||
symbol = stringReference("USDT"),
|
||||
tokenName = stringReference("TETHER"),
|
||||
currencyIconState = CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
earnValue = stringReference("APY 6.54%"),
|
||||
earnType = stringReference("Yield"),
|
||||
onItemClick = {},
|
||||
)
|
||||
}.toPersistentList()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class EarnListUM(
|
||||
val items: ImmutableList<EarnListItemUM>,
|
||||
val contentState: EarnListContentState,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal data class EarnListItemUM(
|
||||
val network: TextReference.Str,
|
||||
val symbol: String,
|
||||
val tokenName: String,
|
||||
val currencyIconState: CurrencyIconState,
|
||||
val earnValue: EarnValueUM,
|
||||
val earnType: EarnType,
|
||||
val onItemClick: () -> Unit,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal data class EarnValueUM(
|
||||
val percent: SerializedBigDecimal,
|
||||
val earnValueType: EarnValueType,
|
||||
)
|
||||
|
||||
internal enum class EarnType {
|
||||
Staking, Yield
|
||||
}
|
||||
|
||||
internal enum class EarnValueType {
|
||||
APR, APY
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal sealed interface EarnListContentState {
|
||||
data object Loading : EarnListContentState
|
||||
data object Content : EarnListContentState
|
||||
data class Error(val onRetryClicked: () -> Unit) : EarnListContentState
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.common.ui.markets.models.MarketsListItemUM
|
|||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.earn.state.EarnListUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -17,6 +18,7 @@ internal data class FeedListUM(
|
|||
val trendingArticle: ArticleConfigUM?,
|
||||
val marketChartConfig: MarketChartConfig,
|
||||
val globalState: GlobalFeedState = GlobalFeedState.Content,
|
||||
val earnListUM: EarnListUM?,
|
||||
)
|
||||
|
||||
internal data class FeedListCallbacks(
|
||||
|
|
@ -28,6 +30,7 @@ internal data class FeedListCallbacks(
|
|||
val onSortTypeClick: (sortBy: SortByTypeUM) -> Unit,
|
||||
val onSliderScroll: () -> Unit,
|
||||
val onSliderEndReached: () -> Unit,
|
||||
val onOpenEarnPageClick: () -> Unit,
|
||||
)
|
||||
|
||||
internal data class FeedListSearchBar(
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.detailed.components.*
|
||||
import com.tangem.core.ui.R as CoreR
|
||||
import com.tangem.features.feed.ui.market.detailed.preview.MarketsTokenDetailsPreview
|
||||
import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent
|
||||
import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent
|
||||
|
|
@ -150,6 +151,7 @@ internal fun MarketsTokenDetailsTopBar(
|
|||
tokenPrice: String,
|
||||
isBackButtonEnabled: Boolean,
|
||||
onBackClick: () -> Unit,
|
||||
onShareClick: () -> Unit,
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.drawBehind { drawRect(backgroundColor) },
|
||||
|
|
@ -159,6 +161,10 @@ internal fun MarketsTokenDetailsTopBar(
|
|||
onBackClicked = onBackClick,
|
||||
enabled = isBackButtonEnabled,
|
||||
),
|
||||
endButton = TopAppBarButtonUM.Icon(
|
||||
iconRes = CoreR.drawable.ic_share_24,
|
||||
onClicked = onShareClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ internal object MarketsTokenDetailsPreview {
|
|||
onFirstVisible = {},
|
||||
onScroll = {},
|
||||
),
|
||||
onShareClick = {},
|
||||
)
|
||||
|
||||
val contentState = MarketsTokenDetailsUM(
|
||||
|
|
@ -141,5 +142,6 @@ internal object MarketsTokenDetailsPreview {
|
|||
onFirstVisible = {},
|
||||
onScroll = {},
|
||||
),
|
||||
onShareClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ internal data class MarketsTokenDetailsUM(
|
|||
val shouldShowPriceSubtitle: Boolean,
|
||||
val onShouldShowPriceSubtitleChange: (Boolean) -> Unit,
|
||||
val relatedNews: RelatedNews,
|
||||
val onShareClick: () -> Unit,
|
||||
) {
|
||||
|
||||
data class ChartState(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue