diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt index 0e48d4e0f4..47ec19a2ff 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt @@ -24,6 +24,7 @@ data class EarnTokenResponse( @Json(name = "symbol") val symbol: String, @Json(name = "name") val name: String, @Json(name = "address") val address: String, + @Json(name = "decimalCount") val decimalCount: Int? = null, ) @JsonClass(generateAdapter = true) diff --git a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt index ccf38c8902..6bde739432 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt @@ -1,6 +1,5 @@ package com.tangem.data.earn.datastore -import arrow.core.Either import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.domain.models.earn.EarnNetworks import javax.inject.Inject @@ -8,6 +7,6 @@ import javax.inject.Singleton @Singleton class EarnNetworksStore @Inject constructor() : - RuntimeStateStore by RuntimeStateStore( - defaultValue = Either.Right(emptyList()), + RuntimeStateStore by RuntimeStateStore( + defaultValue = null, ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt index dded4c2c9b..29f0cbc12e 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt @@ -1,6 +1,5 @@ package com.tangem.data.earn.datastore -import arrow.core.Either import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.domain.models.earn.EarnTopToken import javax.inject.Inject @@ -8,6 +7,6 @@ import javax.inject.Singleton @Singleton class EarnTopTokensStore @Inject constructor() : - RuntimeStateStore by RuntimeStateStore( - defaultValue = Either.Right(emptyList()), + RuntimeStateStore by RuntimeStateStore( + defaultValue = null, ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt b/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt index 9cecd0e002..19674a4f95 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt @@ -1,11 +1,11 @@ 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.repository.DefaultEarnRepository -import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.data.earn.datastore.EarnNetworksStore 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.earn.EarnErrorResolver import com.tangem.domain.earn.repository.EarnRepository @@ -29,19 +29,19 @@ internal object EarnDataModule { tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider, userWalletsListRepository: UserWalletsListRepository, - cryptoCurrencyFactory: CryptoCurrencyFactory, earnNetworksStore: EarnNetworksStore, earnTopTokensStore: EarnTopTokensStore, earnErrorResolver: EarnErrorResolver, + excludedBlockchains: ExcludedBlockchains, ): EarnRepository { return DefaultEarnRepository( tangemTechApi = tangemTechApi, dispatchers = dispatchers, userWalletsListRepository = userWalletsListRepository, - cryptoCurrencyFactory = cryptoCurrencyFactory, earnNetworksStore = earnNetworksStore, earnTopTokensStore = earnTopTokensStore, earnErrorResolver = earnErrorResolver, + excludedBlockchains = excludedBlockchains, ) } } \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt index 98e48ffd57..7a78b887bb 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt @@ -2,6 +2,7 @@ package com.tangem.data.earn.repository import arrow.core.Either import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.earn.converter.EarnTokenConverter @@ -35,12 +36,16 @@ internal class DefaultEarnRepository( private val tangemTechApi: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, private val userWalletsListRepository: UserWalletsListRepository, - private val cryptoCurrencyFactory: CryptoCurrencyFactory, private val earnNetworksStore: EarnNetworksStore, private val earnTopTokensStore: EarnTopTokensStore, private val earnErrorResolver: EarnErrorResolver, + private val excludedBlockchains: ExcludedBlockchains, ) : EarnRepository { + private val cryptoCurrencyFactory: CryptoCurrencyFactory by lazy { + CryptoCurrencyFactory(excludedBlockchains = excludedBlockchains) + } + override fun getEarnTokensBatchFlow(context: EarnTokensBatchingContext, batchSize: Int): EarnTokensBatchFlow { val batchFetcher = EarnTokensBatchFetcher( tangemTechApi = tangemTechApi, @@ -73,7 +78,7 @@ internal class DefaultEarnRepository( } } - override fun observeEarnNetworks(): Flow { + override fun observeEarnNetworks(): Flow { return earnNetworksStore.get() } @@ -100,6 +105,7 @@ internal class DefaultEarnRepository( EarnTokenWithCurrency( earnToken = earnToken, cryptoCurrency = cryptoCurrency, + networkName = Blockchain.fromNetworkId(dto.networkId)?.fullName.orEmpty(), ) } } @@ -110,7 +116,7 @@ internal class DefaultEarnRepository( } } - override fun observeTopEarnTokens(): Flow { + override fun observeTopEarnTokens(): Flow { return earnTopTokensStore.get() } @@ -150,7 +156,7 @@ internal fun createCryptoCurrencyForEarnToken( rawId = CryptoCurrency.RawID(earnToken.token.id), name = earnToken.token.name, symbol = earnToken.token.symbol, - decimals = blockchain.decimals(), + decimals = earnToken.token.decimalCount ?: blockchain.decimals(), contractAddress = earnToken.token.address, ) } diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt index 0495ea6c6c..85d104f727 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt @@ -1,5 +1,7 @@ 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.earn.converter.EarnTokenConverter import com.tangem.data.earn.repository.DefaultEarnRepository.Companion.FIRST_PAGE @@ -88,6 +90,7 @@ internal class EarnTokensBatchFetcher( EarnTokenWithCurrency( earnToken = earnToken, cryptoCurrency = cryptoCurrency, + networkName = Blockchain.fromNetworkId(dto.networkId)?.fullName.orEmpty(), ) } } diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt index 16d4ad67cb..5727b0cd34 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt @@ -15,12 +15,12 @@ interface EarnRepository { */ suspend fun fetchEarnNetworks(type: String) - fun observeEarnNetworks(): Flow + fun observeEarnNetworks(): Flow /** * Fetch top N earn tokens by isForEarn = true and hold it in runtime store. */ suspend fun fetchTopEarnTokens(limit: Int) - fun observeTopEarnTokens(): Flow + fun observeTopEarnTokens(): Flow } \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt index c5e240bf47..1988d0e166 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.earn.usecase +import arrow.core.Either import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.earn.repository.EarnRepository import com.tangem.domain.models.earn.EarnNetwork @@ -27,11 +28,11 @@ class GetEarnNetworksUseCase( earnRepository.observeEarnNetworks(), observeMyNetworkIds(), ) { earn, myNetworkIds -> - earn.map { earnNetworks -> + earn?.map { earnNetworks -> earnNetworks.map { network -> network.copy(isAdded = network.networkId in myNetworkIds) } - } + } ?: Either.Right(emptyList()) }.distinctUntilChanged() } diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt index b4a54344f2..65fb391201 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt @@ -9,7 +9,7 @@ class GetTopEarnTokensUseCase( private val repository: EarnRepository, ) { - operator fun invoke(): Flow { + operator fun invoke(): Flow { return repository.observeTopEarnTokens().distinctUntilChanged() } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt index 5111c260d4..877be4b05a 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt @@ -11,6 +11,7 @@ import kotlinx.serialization.Serializable */ @Serializable data class EarnTokenWithCurrency( + val networkName: String, val earnToken: EarnToken, val cryptoCurrency: CryptoCurrency, ) \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt index 713ce93cf1..f901b35f54 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponent { @@ -22,6 +23,7 @@ interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponen fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) } + @Serializable data class TokenToAdd( val network: TokenMarketInfo.Network, val id: CryptoCurrency.RawID, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt index b673414488..c6b6d1b45a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt @@ -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( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt index 87f135e933..0f988ac0bb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt @@ -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() } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 3cb3671be5..27a5de8ffe 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -4,59 +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( @@ -167,6 +146,13 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { trendingArticle = state.trendingArticle, ) + EarnBlock( + onSeeAllClick = { + // TODO handle in [REDACTED_TASK_KEY] + }, + earnListUM = null, // TODO in [REDACTED_TASK_KEY] + ) + MarketPulseBlock( marketChartConfig = state.marketChartConfig, 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) @Composable private fun FeedListPreview() { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt new file mode 100644 index 0000000000..c43c6b6a12 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt @@ -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, + ), + ) + } + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/ChartsFilterChip.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/ChartsFilterChip.kt new file mode 100644 index 0000000000..4f36a6b094 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/ChartsFilterChip.kt @@ -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 + }, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt new file mode 100644 index 0000000000..de6399ee68 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt @@ -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, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt new file mode 100644 index 0000000000..129ef7438d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt @@ -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) { + Column(modifier = Modifier.fillMaxWidth()) { + items.fastForEach { item -> + EarnListItem(item = item) + } + } +} + +private const val PLACEHOLDER_ITEM_COUNT = 5 \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListError.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListError.kt new file mode 100644 index 0000000000..bcf02de5af --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListError.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt similarity index 91% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt index 0788fe844c..1c2798cbf8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt @@ -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() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt new file mode 100644 index 0000000000..1aa00e6976 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt @@ -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 \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt new file mode 100644 index 0000000000..e2fa4ecbf6 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt @@ -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) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/EarnListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/EarnListUM.kt deleted file mode 100644 index f703b958c0..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/EarnListUM.kt +++ /dev/null @@ -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, - 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 -} \ No newline at end of file