Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-05 15:13:10 +01:00
parent 52e7b5e58c
commit 27921d8c3a
23 changed files with 674 additions and 497 deletions

View file

@ -24,6 +24,7 @@ data class EarnTokenResponse(
@Json(name = "symbol") val symbol: String, @Json(name = "symbol") val symbol: String,
@Json(name = "name") val name: String, @Json(name = "name") val name: String,
@Json(name = "address") val address: String, @Json(name = "address") val address: String,
@Json(name = "decimalCount") val decimalCount: Int? = null,
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)

View file

@ -1,6 +1,5 @@
package com.tangem.data.earn.datastore package com.tangem.data.earn.datastore
import arrow.core.Either
import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.domain.models.earn.EarnNetworks import com.tangem.domain.models.earn.EarnNetworks
import javax.inject.Inject import javax.inject.Inject
@ -8,6 +7,6 @@ import javax.inject.Singleton
@Singleton @Singleton
class EarnNetworksStore @Inject constructor() : class EarnNetworksStore @Inject constructor() :
RuntimeStateStore<EarnNetworks> by RuntimeStateStore( RuntimeStateStore<EarnNetworks?> by RuntimeStateStore(
defaultValue = Either.Right(emptyList()), defaultValue = null,
) )

View file

@ -1,6 +1,5 @@
package com.tangem.data.earn.datastore package com.tangem.data.earn.datastore
import arrow.core.Either
import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.domain.models.earn.EarnTopToken import com.tangem.domain.models.earn.EarnTopToken
import javax.inject.Inject import javax.inject.Inject
@ -8,6 +7,6 @@ import javax.inject.Singleton
@Singleton @Singleton
class EarnTopTokensStore @Inject constructor() : class EarnTopTokensStore @Inject constructor() :
RuntimeStateStore<EarnTopToken> by RuntimeStateStore( RuntimeStateStore<EarnTopToken?> by RuntimeStateStore(
defaultValue = Either.Right(emptyList()), defaultValue = null,
) )

View file

@ -1,11 +1,11 @@
package com.tangem.data.earn.di package com.tangem.data.earn.di
import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.data.earn.DefaultEarnErrorResolver import com.tangem.data.earn.DefaultEarnErrorResolver
import com.tangem.data.earn.repository.DefaultEarnRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.data.earn.datastore.EarnNetworksStore import com.tangem.data.earn.datastore.EarnNetworksStore
import com.tangem.data.earn.datastore.EarnTopTokensStore import com.tangem.data.earn.datastore.EarnTopTokensStore
import com.tangem.data.earn.repository.DefaultEarnRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.earn.EarnErrorResolver import com.tangem.domain.earn.EarnErrorResolver
import com.tangem.domain.earn.repository.EarnRepository import com.tangem.domain.earn.repository.EarnRepository
@ -29,19 +29,19 @@ internal object EarnDataModule {
tangemTechApi: TangemTechApi, tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
userWalletsListRepository: UserWalletsListRepository, userWalletsListRepository: UserWalletsListRepository,
cryptoCurrencyFactory: CryptoCurrencyFactory,
earnNetworksStore: EarnNetworksStore, earnNetworksStore: EarnNetworksStore,
earnTopTokensStore: EarnTopTokensStore, earnTopTokensStore: EarnTopTokensStore,
earnErrorResolver: EarnErrorResolver, earnErrorResolver: EarnErrorResolver,
excludedBlockchains: ExcludedBlockchains,
): EarnRepository { ): EarnRepository {
return DefaultEarnRepository( return DefaultEarnRepository(
tangemTechApi = tangemTechApi, tangemTechApi = tangemTechApi,
dispatchers = dispatchers, dispatchers = dispatchers,
userWalletsListRepository = userWalletsListRepository, userWalletsListRepository = userWalletsListRepository,
cryptoCurrencyFactory = cryptoCurrencyFactory,
earnNetworksStore = earnNetworksStore, earnNetworksStore = earnNetworksStore,
earnTopTokensStore = earnTopTokensStore, earnTopTokensStore = earnTopTokensStore,
earnErrorResolver = earnErrorResolver, earnErrorResolver = earnErrorResolver,
excludedBlockchains = excludedBlockchains,
) )
} }
} }

View file

@ -2,6 +2,7 @@ package com.tangem.data.earn.repository
import arrow.core.Either import arrow.core.Either
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.earn.converter.EarnTokenConverter import com.tangem.data.earn.converter.EarnTokenConverter
@ -35,12 +36,16 @@ internal class DefaultEarnRepository(
private val tangemTechApi: TangemTechApi, private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
private val userWalletsListRepository: UserWalletsListRepository, private val userWalletsListRepository: UserWalletsListRepository,
private val cryptoCurrencyFactory: CryptoCurrencyFactory,
private val earnNetworksStore: EarnNetworksStore, private val earnNetworksStore: EarnNetworksStore,
private val earnTopTokensStore: EarnTopTokensStore, private val earnTopTokensStore: EarnTopTokensStore,
private val earnErrorResolver: EarnErrorResolver, private val earnErrorResolver: EarnErrorResolver,
private val excludedBlockchains: ExcludedBlockchains,
) : EarnRepository { ) : EarnRepository {
private val cryptoCurrencyFactory: CryptoCurrencyFactory by lazy {
CryptoCurrencyFactory(excludedBlockchains = excludedBlockchains)
}
override fun getEarnTokensBatchFlow(context: EarnTokensBatchingContext, batchSize: Int): EarnTokensBatchFlow { override fun getEarnTokensBatchFlow(context: EarnTokensBatchingContext, batchSize: Int): EarnTokensBatchFlow {
val batchFetcher = EarnTokensBatchFetcher( val batchFetcher = EarnTokensBatchFetcher(
tangemTechApi = tangemTechApi, tangemTechApi = tangemTechApi,
@ -73,7 +78,7 @@ internal class DefaultEarnRepository(
} }
} }
override fun observeEarnNetworks(): Flow<EarnNetworks> { override fun observeEarnNetworks(): Flow<EarnNetworks?> {
return earnNetworksStore.get() return earnNetworksStore.get()
} }
@ -100,6 +105,7 @@ internal class DefaultEarnRepository(
EarnTokenWithCurrency( EarnTokenWithCurrency(
earnToken = earnToken, earnToken = earnToken,
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
networkName = Blockchain.fromNetworkId(dto.networkId)?.fullName.orEmpty(),
) )
} }
} }
@ -110,7 +116,7 @@ internal class DefaultEarnRepository(
} }
} }
override fun observeTopEarnTokens(): Flow<EarnTopToken> { override fun observeTopEarnTokens(): Flow<EarnTopToken?> {
return earnTopTokensStore.get() return earnTopTokensStore.get()
} }
@ -150,7 +156,7 @@ internal fun createCryptoCurrencyForEarnToken(
rawId = CryptoCurrency.RawID(earnToken.token.id), rawId = CryptoCurrency.RawID(earnToken.token.id),
name = earnToken.token.name, name = earnToken.token.name,
symbol = earnToken.token.symbol, symbol = earnToken.token.symbol,
decimals = blockchain.decimals(), decimals = earnToken.token.decimalCount ?: blockchain.decimals(),
contractAddress = earnToken.token.address, contractAddress = earnToken.token.address,
) )
} }

View file

@ -1,5 +1,7 @@
package com.tangem.data.earn.repository.batch package com.tangem.data.earn.repository.batch
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.earn.converter.EarnTokenConverter import com.tangem.data.earn.converter.EarnTokenConverter
import com.tangem.data.earn.repository.DefaultEarnRepository.Companion.FIRST_PAGE import com.tangem.data.earn.repository.DefaultEarnRepository.Companion.FIRST_PAGE
@ -88,6 +90,7 @@ internal class EarnTokensBatchFetcher(
EarnTokenWithCurrency( EarnTokenWithCurrency(
earnToken = earnToken, earnToken = earnToken,
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
networkName = Blockchain.fromNetworkId(dto.networkId)?.fullName.orEmpty(),
) )
} }
} }

View file

@ -15,12 +15,12 @@ interface EarnRepository {
*/ */
suspend fun fetchEarnNetworks(type: String) suspend fun fetchEarnNetworks(type: String)
fun observeEarnNetworks(): Flow<EarnNetworks> fun observeEarnNetworks(): Flow<EarnNetworks?>
/** /**
* Fetch top N earn tokens by isForEarn = true and hold it in runtime store. * Fetch top N earn tokens by isForEarn = true and hold it in runtime store.
*/ */
suspend fun fetchTopEarnTokens(limit: Int) suspend fun fetchTopEarnTokens(limit: Int)
fun observeTopEarnTokens(): Flow<EarnTopToken> fun observeTopEarnTokens(): Flow<EarnTopToken?>
} }

View file

@ -1,5 +1,6 @@
package com.tangem.domain.earn.usecase package com.tangem.domain.earn.usecase
import arrow.core.Either
import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.earn.repository.EarnRepository import com.tangem.domain.earn.repository.EarnRepository
import com.tangem.domain.models.earn.EarnNetwork import com.tangem.domain.models.earn.EarnNetwork
@ -27,11 +28,11 @@ class GetEarnNetworksUseCase(
earnRepository.observeEarnNetworks(), earnRepository.observeEarnNetworks(),
observeMyNetworkIds(), observeMyNetworkIds(),
) { earn, myNetworkIds -> ) { earn, myNetworkIds ->
earn.map { earnNetworks -> earn?.map { earnNetworks ->
earnNetworks.map { network -> earnNetworks.map { network ->
network.copy(isAdded = network.networkId in myNetworkIds) network.copy(isAdded = network.networkId in myNetworkIds)
} }
} } ?: Either.Right(emptyList())
}.distinctUntilChanged() }.distinctUntilChanged()
} }

View file

@ -9,7 +9,7 @@ class GetTopEarnTokensUseCase(
private val repository: EarnRepository, private val repository: EarnRepository,
) { ) {
operator fun invoke(): Flow<EarnTopToken> { operator fun invoke(): Flow<EarnTopToken?> {
return repository.observeTopEarnTokens().distinctUntilChanged() return repository.observeTopEarnTokens().distinctUntilChanged()
} }
} }

View file

@ -11,6 +11,7 @@ import kotlinx.serialization.Serializable
*/ */
@Serializable @Serializable
data class EarnTokenWithCurrency( data class EarnTokenWithCurrency(
val networkName: String,
val earnToken: EarnToken, val earnToken: EarnToken,
val cryptoCurrency: CryptoCurrency, val cryptoCurrency: CryptoCurrency,
) )

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponent { interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponent {
@ -22,6 +23,7 @@ interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponen
fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId)
} }
@Serializable
data class TokenToAdd( data class TokenToAdd(
val network: TokenMarketInfo.Network, val network: TokenMarketInfo.Network,
val id: CryptoCurrency.RawID, val id: CryptoCurrency.RawID,

View file

@ -112,8 +112,10 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
// suspend until all required data is selected // suspend until all required data is selected
val (selectedNetworkValue, selectedPortfolioValue) = allRequireForAdd.first() val (selectedNetworkValue, selectedPortfolioValue) = allRequireForAdd.first()
val isTokenAlreadyAdded = selectedPortfolioValue.account.addedMarketNetworks val isTokenAlreadyAdded = getAccountCurrencyStatusUseCase.invokeSync(
.any { it.networkId == selectedNetworkValue.selectedNetwork.networkId } userWalletId = selectedPortfolioValue.userWallet.walletId,
currency = selectedNetworkValue.cryptoCurrency,
).isSome()
if (isTokenAlreadyAdded) { if (isTokenAlreadyAdded) {
finishSuccessFlow( finishSuccessFlow(
@ -165,7 +167,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
): AvailableToAddData? { ): AvailableToAddData? {
val portfolioData = portfolioFetcher.data.firstOrNull() ?: return null 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 wallet = balance.userWallet
val accounts = balance.accountsBalance.accountStatuses val accounts = balance.accountsBalance.accountStatuses
@ -206,9 +208,9 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
) )
}.toMap() }.toMap()
if (availableToOpenWallets.isEmpty()) return null if (availableToAddInWallets.isEmpty()) return null
return AvailableToAddData(availableToAddWallets = availableToOpenWallets) return AvailableToAddData(availableToAddWallets = availableToAddInWallets)
} }
private suspend fun createCryptoCurrency( private suspend fun createCryptoCurrency(

View file

@ -17,11 +17,11 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
@Composable @Composable
internal fun EarnListPlaceholder(modifier: Modifier = Modifier) { internal fun EarnListPlaceholder(modifier: Modifier = Modifier, placeholderCount: Int = PLACEHOLDER_ITEMS_COUNT) {
Column( Column(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) { ) {
repeat(PLACEHOLDER_ITEMS_COUNT) { repeat(placeholderCount) {
EarnItemPlaceholder() EarnItemPlaceholder()
} }
} }

View file

@ -4,59 +4,38 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.* import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind 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.graphics.vector.ImageVector
import androidx.compose.ui.layout.onFirstVisible
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp 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.R
import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW 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.conditional
import com.tangem.core.ui.extensions.resolveReference 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.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BaseSearchBarTestTags.SEARCH_BAR import com.tangem.core.ui.test.BaseSearchBarTestTags.SEARCH_BAR
import com.tangem.features.feed.model.market.list.state.SortByTypeUM 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.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 @Composable
internal fun FeedListHeader( internal fun FeedListHeader(
@ -167,6 +146,13 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
trendingArticle = state.trendingArticle, trendingArticle = state.trendingArticle,
) )
EarnBlock(
onSeeAllClick = {
// TODO handle in [REDACTED_TASK_KEY]
},
earnListUM = null, // TODO in [REDACTED_TASK_KEY]
)
MarketPulseBlock( MarketPulseBlock(
marketChartConfig = state.marketChartConfig, marketChartConfig = state.marketChartConfig,
feedListCallbacks = state.feedListCallbacks, feedListCallbacks = state.feedListCallbacks,
@ -174,400 +160,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.primary),
) {
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) @Preview(showBackground = true, heightDp = 1500)
@Composable @Composable
private fun FeedListPreview() { private fun FeedListPreview() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.layout.*
import androidx.compose.foundation.lazy.LazyRow 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.RectangleShimmer
import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.BlockCard 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 import com.tangem.core.ui.res.TangemThemePreview
@Composable @Composable
@ -94,7 +96,10 @@ internal fun NewsLoadingBlock() {
@Composable @Composable
private fun ChartsLoading(modifier: Modifier = Modifier) { private fun ChartsLoading(modifier: Modifier = Modifier) {
BlockCard(modifier) { BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View file

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

View file

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

View file

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