From bc76eaa64a470718326c5820d910542d39801fb7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Dec 2025 10:01:22 +0100 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tangem/tap/di/domain/NewsDomainModule.kt | 11 +- .../com/tangem/common/ui/news/ArticleCard.kt | 11 +- .../common/ui/news/ArticleLoadingCard.kt | 63 +++++++++ .../com/tangem/datasource/api/news/NewsApi.kt | 2 +- .../models/response/NewsTrendingResponse.kt | 6 - .../bottomsheets/state}/BottomSheetState.kt | 2 +- .../tangem/core/ui/components/label/Label.kt | 31 ++-- .../ui/components/label/entity/LabelUM.kt | 2 +- .../core/ui/utils/DateTimeFormatters.kt | 11 ++ .../news/repository/DefaultNewsRepository.kt | 7 +- .../domain/news/repository/NewsRepository.kt | 4 +- .../news/usecase/FetchTrendingNewsUseCase.kt | 23 +++ .../entry/components/FeedEntryComponent.kt | 2 +- features/feed/impl/build.gradle.kts | 2 + .../components/DefaultFeedEntryComponent.kt | 20 ++- .../feed/components/FeedEntryChildFactory.kt | 6 +- .../components/feed/DefaultFeedComponent.kt | 18 ++- .../features/feed/di/ComponentModule.kt | 18 +++ .../tangem/features/feed/di/ModelModule.kt | 20 +++ .../feed/model/feed/FeedComponentModel.kt | 103 ++++++++++++++ .../feed/ui/EntryBottomSheetContent.kt | 48 +++++++ .../tangem/features/feed/ui/feed/FeedList.kt | 132 ++++++++++-------- .../features/feed/ui/feed/FeedListLoading.kt | 100 +++++++++++++ .../preview/FeedListPreviewDataProvider.kt | 9 +- .../features/feed/ui/feed/state/FeedListUM.kt | 10 +- .../ui/feed/state/SearchBarStateFactory.kt | 24 ++++ .../ui/feed/state/TrendingNewsStateFactory.kt | 83 +++++++++++ .../details/MarketsTokenDetailsComponent.kt | 2 +- .../markets/entry/BottomSheetState.kt | 6 - .../markets/entry/MarketsEntryComponent.kt | 1 + .../tokenlist/MarketsTokenListComponent.kt | 2 +- .../DefaultMarketsTokenDetailsComponent.kt | 2 +- .../impl/DefaultMarketsEntryComponent.kt | 2 +- .../entry/impl/ui/EntryBottomSheetContent.kt | 2 +- .../impl/DefaultMarketsTokenListComponent.kt | 2 +- .../tokenlist/impl/model/MarketsListModel.kt | 2 +- .../markets/tokenlist/impl/ui/MarketsList.kt | 2 +- features/wallet/impl/build.gradle.kts | 1 + .../wallet/child/wallet/WalletComponent.kt | 54 ++++++- .../wallet/child/wallet/model/WalletModel.kt | 9 ++ .../common/preview/WalletScreenPreviewData.kt | 1 + .../wallet/state/WalletStateController.kt | 1 + .../wallet/state/model/WalletScreenState.kt | 1 + .../presentation/wallet/ui/WalletScreen.kt | 55 ++++---- .../extension/BaseExtensionConfigurations.kt | 4 +- 46 files changed, 764 insertions(+), 154 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt rename {features/feed/api/src/main/kotlin/com/tangem/features/feed/entry => core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/state}/BottomSheetState.kt (51%) create mode 100644 domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryBottomSheetContent.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/SearchBarStateFactory.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt delete mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b5b03c3345..bb2b8ab488 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -215,6 +215,7 @@ dependencies { implementation(projects.data.walletManager) implementation(projects.data.yieldSupply) implementation(projects.data.hotWallet) + implementation(projects.data.news) /** Features */ implementation(projects.features.referral.impl) diff --git a/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt index dda3134a35..55b1457a55 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NewsDomainModule.kt @@ -1,10 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.news.repository.NewsRepository -import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase -import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase -import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase -import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase +import com.tangem.domain.news.usecase.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -38,4 +35,10 @@ internal object NewsDomainModule { fun provideGetNewsListBatchFlowUseCase(repository: NewsRepository): GetNewsListBatchFlowUseCase { return GetNewsListBatchFlowUseCase(repository) } + + @Provides + @Singleton + fun provideFetchTrendingNewsUseCase(repository: NewsRepository): FetchTrendingNewsUseCase { + return FetchTrendingNewsUseCase(repository) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt index a1ee6a610c..70d50e3b81 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CardColors import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -17,6 +18,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.R 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.components.label.Label import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference @@ -30,10 +32,16 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet @Composable -fun ArticleCard(articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier) { +fun ArticleCard( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, + colors: CardColors = TangemBlockCardColors, +) { BlockCard( modifier = modifier, onClick = onArticleClick, + colors = colors, ) { if (articleConfigUM.isTrending) { TrendingArticle(articleConfigUM = articleConfigUM) @@ -167,7 +175,6 @@ private fun ArticleCardsPreview() { LabelUM(TextReference.Str("BTC")), LabelUM(TextReference.Str("Supply")), LabelUM(TextReference.Str("Demand")), - LabelUM(TextReference.Str("Best rate")), LabelUM(TextReference.Str("Breaking news")), ).toImmutableSet() diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt new file mode 100644 index 0000000000..4ef5f1ce75 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt @@ -0,0 +1,63 @@ +package com.tangem.common.ui.news + +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.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 + +@Composable +fun TrendingLoadingArticle(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp, horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + RectangleShimmer(modifier = Modifier.size(width = 96.dp, height = 24.dp), radius = 8.dp) + SpacerH(12.dp) + RectangleShimmer(modifier = Modifier.size(width = 285.dp, height = 18.dp), radius = 4.dp) + SpacerH(6.dp) + RectangleShimmer(modifier = Modifier.size(width = 190.dp, height = 18.dp), radius = 4.dp) + SpacerH(14.dp) + RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 18.dp), radius = 4.dp) + SpacerH(32.dp) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) + RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) + RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 24.dp), radius = 8.dp) + } + } + } +} + +@Composable +fun DefaultLoadingArticle() { + BlockCard( + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { + Column(modifier = Modifier.padding(12.dp)) { + RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp) + SpacerH(12.dp) + RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp) + SpacerH(6.dp) + RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp) + SpacerH(6.dp) + RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp) + SpacerH(16.dp) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp) + RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp) + } + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt index 24e359b20e..d0e5ab9707 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/news/NewsApi.kt @@ -38,6 +38,6 @@ interface NewsApi { private companion object { - private const val NEWS_PATH = "api/v1/news" + private const val NEWS_PATH = "v1/news" } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt index 3a21af3d04..91464cb405 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/news/models/response/NewsTrendingResponse.kt @@ -5,11 +5,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class NewsTrendingResponse( - @Json(name = "meta") val meta: NewsTrendingMetaDto, @Json(name = "items") val items: List, -) - -@JsonClass(generateAdapter = true) -data class NewsTrendingMetaDto( - @Json(name = "limit") val limit: Int, ) \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/state/BottomSheetState.kt similarity index 51% rename from features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/state/BottomSheetState.kt index 9b58eb7690..70d201ddd5 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/BottomSheetState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/state/BottomSheetState.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.entry +package com.tangem.core.ui.components.bottomsheets.state enum class BottomSheetState { EXPANDED, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt index 36e1eae057..0f6b07e0d1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -17,13 +18,15 @@ 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.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest import com.tangem.core.ui.R +import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelSize import com.tangem.core.ui.components.label.entity.LabelStyle @@ -108,11 +111,23 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) { state.leadingContent.let { leadingContentUM -> when (leadingContentUM) { is LabelLeadingContentUM.Token -> { - Icon( - modifier = Modifier - .size(16.dp), - painter = painterResource(leadingContentUM.icon), - tint = Color.Unspecified, + SubcomposeAsyncImage( + modifier = Modifier.size(16.dp), + model = ImageRequest.Builder(context = LocalContext.current) + .data(leadingContentUM.iconUrl) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { CircleShimmer() }, + error = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.tertiary, + shape = CircleShape, + ), + ) + }, contentDescription = null, ) } @@ -168,7 +183,7 @@ private fun LabelPreview() { Label( state = LabelUM( leadingContent = LabelLeadingContentUM.Token( - icon = R.drawable.img_bsc_22, + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/euro-coin.png", ), text = TextReference.Str("Regular Label"), style = LabelStyle.REGULAR, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt index afa0e60bae..131469bd96 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt @@ -17,7 +17,7 @@ data class LabelUM( @Immutable sealed class LabelLeadingContentUM { data object None : LabelLeadingContentUM() - data class Token(val icon: Int) : LabelLeadingContentUM() + data class Token(val iconUrl: String) : LabelLeadingContentUM() } enum class LabelStyle { diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 85d0294996..8a5cae1ae4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -1,6 +1,10 @@ package com.tangem.core.ui.utils import android.text.format.DateFormat +import com.tangem.core.ui.utils.DateTimeFormatters.dateDDMMYYYY +import com.tangem.core.ui.utils.DateTimeFormatters.dateMMMdd +import com.tangem.core.ui.utils.DateTimeFormatters.dateTimeFormatter +import com.tangem.core.ui.utils.DateTimeFormatters.dateYYYY import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormatter @@ -80,6 +84,13 @@ object DateTimeFormatters { getBestFormatterBySkeleton("yyyy") } + /** + * Example: "June 31" + */ + val dateDMMM: DateTimeFormatter by lazy { + getBestFormatterBySkeleton("d MMMM") + } + /** * Example: "31.06.2020 12:00", "06/31/2020 12:00", "06/31/2020 12:00 PM" */ diff --git a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt index e42068534e..426bc6f287 100644 --- a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt +++ b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt @@ -18,7 +18,6 @@ import com.tangem.pagination.fetcher.BatchFetcher import com.tangem.pagination.toBatchFlow import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -31,7 +30,7 @@ import kotlin.collections.orEmpty * Implementation of [NewsRepository]. [REDACTED_AUTHOR] */ -internal class DefaultNewsRepository @Inject constructor( +internal class DefaultNewsRepository( private val newsApi: NewsApi, private val dispatchers: CoroutineDispatcherProvider, private val newsDetailsStore: NewsDetailsStore, @@ -68,8 +67,8 @@ internal class DefaultNewsRepository @Inject constructor( fetchDetailedArticlesInternal(newsIds = newsIds, language = language) } - override suspend fun getTrendingNews(limit: Int, language: String?): List { - return fetchAndStoreTrendingNews(limit = limit, language = language) + override suspend fun getTrendingNews(limit: Int, language: String?) { + fetchAndStoreTrendingNews(limit = limit, language = language) } override fun observeTrendingNews(): Flow> { diff --git a/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt b/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt index c8ab6c6cd6..7db6669b0d 100644 --- a/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt +++ b/domain/news/src/main/java/com/tangem/domain/news/repository/NewsRepository.kt @@ -38,12 +38,12 @@ interface NewsRepository { suspend fun fetchDetailedArticles(newsIds: Collection, language: String?) /** - * Returns list of trending news by limit and with correct locale. + * Fetch list of trending news by limit and with correct locale and store it in runtime data store. * * @param limit * @param language current device locale */ - suspend fun getTrendingNews(limit: Int, language: String?): List + suspend fun getTrendingNews(limit: Int, language: String?) /** * Observes trending news with runtime viewed flag support. diff --git a/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt b/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt new file mode 100644 index 0000000000..319f111625 --- /dev/null +++ b/domain/news/src/main/java/com/tangem/domain/news/usecase/FetchTrendingNewsUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.news.usecase + +import arrow.core.Either +import com.tangem.domain.news.repository.NewsRepository +import java.util.Locale + +/** + * Fetches trending news to store it in runtime data store. + */ + +class FetchTrendingNewsUseCase(private val newsRepository: NewsRepository) { + + suspend operator fun invoke(): Either = Either.catch { + newsRepository.getTrendingNews( + limit = LIMIT_FOR_TRENDING_NEWS, + language = Locale.getDefault().language, + ) + } + + companion object { + private const val LIMIT_FOR_TRENDING_NEWS = 10 + } +} \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt index ad7aab9506..43d68d3d11 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.features.feed.entry.BottomSheetState +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState @Stable interface FeedEntryComponent { diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index c309d45ccf..cadd191a0e 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.notifications.models) implementation(projects.domain.transaction) + implementation(projects.domain.news) // FIXME [REDACTED_TASK_KEY] // Remove the "Buy" and "Sell" actions from the redux middleware. @@ -86,6 +87,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) + implementation(projects.core.utils) /* Common */ implementation(projects.common.ui) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt index cdf3a46598..d27de2ff81 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -1,10 +1,13 @@ package com.tangem.features.feed.components +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp +import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack @@ -13,11 +16,13 @@ import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.decompose.ComposableModularContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent -import com.tangem.features.feed.entry.BottomSheetState import com.tangem.features.feed.entry.components.FeedEntryComponent +import com.tangem.features.feed.ui.EntryBottomSheetContent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -35,7 +40,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( popCallback = { onChildBack() }, ) - private val stack: Value> = childStack( + private val stack: Value> = childStack( key = "main", source = stackNavigation, serializer = FeedEntryChildFactory.Child.serializer(), @@ -59,7 +64,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier, ) { - bottomSheetState // TODO will be continued in next tasks. + val stackState by stack.subscribeAsState() + + BackHandler(enabled = bottomSheetState.value == BottomSheetState.EXPANDED) { + onChildBack() + } + + EntryBottomSheetContent( + stackState = stackState, + onHeaderSizeChange = onHeaderSizeChange, + ) } private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index c33781008b..6483e57566 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -3,6 +3,7 @@ package com.tangem.features.feed.components import androidx.compose.runtime.Immutable import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.Route +import com.tangem.core.ui.decompose.ComposableModularContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.feed.components.feed.DefaultFeedComponent @@ -11,8 +12,9 @@ import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListCo import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent import com.tangem.features.feed.components.news.list.DefaultNewsListComponent import kotlinx.serialization.Serializable +import javax.inject.Inject -internal class FeedEntryChildFactory { +internal class FeedEntryChildFactory @Inject constructor() { @Serializable @Immutable @@ -43,7 +45,7 @@ internal class FeedEntryChildFactory { child: Child, appComponentContext: AppComponentContext, onTokenClick: (TokenMarketParams, AppCurrency) -> Unit, - ): Any { + ): ComposableModularContentComponent { return when (child) { is Child.TokenDetails -> { DefaultMarketsTokenDetailsComponent( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index a6bd119f49..910542c05a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -1,23 +1,37 @@ package com.tangem.features.feed.components.feed import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.features.feed.model.feed.FeedComponentModel +import com.tangem.features.feed.ui.feed.FeedListContent +import com.tangem.features.feed.ui.feed.FeedListHeader internal class DefaultFeedComponent( appComponentContext: AppComponentContext, ) : ComposableModularContentComponent, AppComponentContext by appComponentContext { + private val feedComponentModel = getOrCreateModel() + @Composable override fun Title() { + val state by feedComponentModel.state.collectAsStateWithLifecycle() + FeedListHeader(state.searchBar) } @Composable override fun Content(modifier: Modifier) { + val state by feedComponentModel.state.collectAsStateWithLifecycle() + FeedListContent( + modifier = modifier, + state = state, + ) } @Composable - override fun Footer() { - } + override fun Footer() = Unit } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt new file mode 100644 index 0000000000..5c06261458 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.feed.di + +import com.tangem.features.feed.components.DefaultFeedEntryComponent +import com.tangem.features.feed.entry.components.FeedEntryComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindFeedEntryComponent(factory: DefaultFeedEntryComponent.Factory): FeedEntryComponent.Factory +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt new file mode 100644 index 0000000000..f00b75a4b1 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.feed.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.feed.model.feed.FeedComponentModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(FeedComponentModel::class) + fun bindsFeedComponentModel(model: FeedComponentModel): Model +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt new file mode 100644 index 0000000000..5445a03eed --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -0,0 +1,103 @@ +package com.tangem.features.feed.model.feed + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase +import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.feed.state.* +import com.tangem.features.feed.ui.market.state.SortByTypeUM +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toPersistentHashMap +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import javax.inject.Inject + +@Stable +@ModelScoped +internal class FeedComponentModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase, + private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase, +) : Model() { + + private val _state = MutableStateFlow(initialState()) + val state = _state.asStateFlow() + + private val searchBarStateFactory by lazy(LazyThreadSafetyMode.NONE) { + SearchBarStateFactory( + currentStateProvider = Provider { _state.value }, + onStateUpdate = { newState -> _state.update { newState } }, + ) + } + + private val trendingNewsStateFactory by lazy(LazyThreadSafetyMode.NONE) { + TrendingNewsStateFactory( + currentStateProvider = Provider { _state.value }, + onStateUpdate = { newState -> _state.update { newState } }, + ) + } + + init { + modelScope.launch(dispatchers.default) { + fetchTrendingNewsUseCase() + subscribeOnTrendingNews() + } + _state.update { feedListUM -> + feedListUM.copy( + searchBar = _state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange), + ) + } + } + + private suspend fun subscribeOnTrendingNews() { + manageTrendingNewsUseCase().collect { articles -> + trendingNewsStateFactory.updateTrendingNewsState(articles) + } + } + + private fun initialState(): FeedListUM { + return FeedListUM( + currentDate = getCurrentDate(), + searchBar = SearchBarUM( + placeholderText = resourceReference(R.string.markets_search_header_title), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = { }, + ), + feedListCallbacks = FeedListCallbacks( + onSearchClick = {}, + onMarketOpenClick = {}, + onArticleClick = {}, + onOpenAllNews = {}, + onMarketItemClick = {}, + onSortTypeClick = {}, + ), + news = NewsUM.Loading, + trendingArticle = null, + marketChartConfig = MarketChartConfig( + marketCharts = buildMap { + SortByTypeUM.entries.forEach { + put(it, MarketChartUM.Loading) + } // TODO in [REDACTED_TASK_KEY] add correct sorting + }.toPersistentHashMap(), + currentSortByType = SortByTypeUM.TopGainers, + ), + ) + } + + private fun getCurrentDate(): String { + val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault()) + return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryBottomSheetContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryBottomSheetContent.kt new file mode 100644 index 0000000000..cf87b0cefd --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryBottomSheetContent.kt @@ -0,0 +1,48 @@ +package com.tangem.features.feed.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.decompose.ComposableModularContentComponent +import com.tangem.features.feed.components.FeedEntryChildFactory + +@Composable +internal fun EntryBottomSheetContent( + stackState: ChildStack, + onHeaderSizeChange: (Dp) -> Unit, +) { + val density = LocalDensity.current + + Scaffold( + contentWindowInsets = WindowInsets(0.dp), + topBar = { + AnimatedContent( + targetState = stackState.active.instance, + modifier = Modifier.onGloballyPositioned { coordinates -> + if (coordinates.size.height > 0) { + with(density) { + onHeaderSizeChange(coordinates.size.height.toDp()) + } + } + }, + ) { currentState -> + currentState.Title() + } + }, + content = { contentPadding -> + AnimatedContent( + stackState.active.instance, + ) { currentState -> + currentState.Content(modifier = Modifier.padding(contentPadding)) + } + }, + ) +} \ No newline at end of file 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 51c97516b7..82cde520e1 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 @@ -22,25 +22,25 @@ 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.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity 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.unit.dp import androidx.compose.ui.util.fastForEach +import com.tangem.common.ui.news.ArticleCard +import com.tangem.common.ui.news.ArticleConfigUM 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.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.components.fields.SearchBar -import com.tangem.common.ui.news.ArticleCard -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.core.ui.components.fields.TangemSearchBarDefaults +import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe @@ -48,51 +48,44 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState -import com.tangem.features.feed.ui.feed.state.FeedListCallbacks -import com.tangem.features.feed.ui.feed.state.FeedListUM -import com.tangem.features.feed.ui.feed.state.MarketChartConfig -import com.tangem.features.feed.ui.feed.state.MarketChartUM +import com.tangem.features.feed.ui.feed.state.* import com.tangem.features.feed.ui.market.components.MarketsListItem import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM -import kotlinx.collections.immutable.ImmutableList @Composable -internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) { - val density = LocalDensity.current +internal fun FeedListHeader(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value + SearchBar( + modifier = modifier + .drawBehind { drawRect(background) } + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp), + state = searchBarUM, + colors = TangemSearchBarDefaults.defaultTextFieldColors.copy( + focusedContainerColor = TangemTheme.colors.field.focused, + unfocusedContainerColor = TangemTheme.colors.field.focused, + ), + ) +} + +@Composable +internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { + val background = LocalMainBottomSheetColor.current.value + Column( modifier = modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .drawBehind { drawRect(background) }, ) { - SearchBar( - modifier = Modifier - .drawBehind { drawRect(background) } - .padding( - start = 16.dp, - end = 16.dp, - bottom = 8.dp, - ) - .onGloballyPositioned { coordinates -> - if (coordinates.size.height > 0) { - with(density) { - onHeaderSizeChange(coordinates.size.height.toDp()) - } - } - } - .padding(bottom = 4.dp), - state = state.searchBar, - ) - SpacerH(20.dp) Text( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = 20.dp), text = stringResourceSafe(R.string.feed_market_and_news), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, @@ -100,7 +93,7 @@ internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modif Text( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = 20.dp), text = state.currentDate, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.tertiary, @@ -156,6 +149,17 @@ private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: @Composable private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { if (marketChartConfig.marketCharts.isNotEmpty()) { + Header( + title = { + Text( + text = stringResourceSafe(R.string.markets_common_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + }, + onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) }, + ) + LazyRow( modifier = Modifier.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, @@ -175,17 +179,6 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb } } - Header( - title = { - Text( - text = stringResourceSafe(R.string.markets_common_title), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - }, - onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) }, - ) - SpacerH(12.dp) AnimatedContent( @@ -207,18 +200,38 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb @Suppress("CanBeNonNullable") @Composable -private fun NewsBlock( +private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { + AnimatedContent(news) { newsUM -> + when (newsUM) { + is NewsUM.Content -> { + if (newsUM.content.isNotEmpty()) { + NewsContentBlock( + feedListCallbacks = feedListCallbacks, + news = newsUM, + trendingArticle = trendingArticle, + ) + } + } + NewsUM.Loading -> { + NewsLoadingBlock() + } + } + } +} + +@Composable +private fun NewsContentBlock( feedListCallbacks: FeedListCallbacks, - news: ImmutableList, + news: NewsUM.Content, trendingArticle: ArticleConfigUM?, ) { - if (news.isNotEmpty()) { + Column { Header( title = { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = stringResourceSafe(R.string.common_news), - style = TangemTheme.typography.subtitle1, + style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) @@ -244,13 +257,12 @@ private fun NewsBlock( append(stringResourceSafe(R.string.feed_tangem_ai)) } }, - style = TangemTheme.typography.subtitle1, + style = TangemTheme.typography.h3, ) } }, onSeeAllClick = feedListCallbacks.onOpenAllNews, ) - SpacerH(12.dp) trendingArticle?.let { article -> @@ -260,8 +272,8 @@ private fun NewsBlock( .padding(horizontal = 16.dp), articleConfigUM = article, onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) - SpacerH(12.dp) } @@ -272,13 +284,16 @@ private fun NewsBlock( state = rememberLazyListState(), ) { items( - items = news, + items = news.content, key = ArticleConfigUM::id, ) { article -> ArticleCard( articleConfigUM = article, onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, - modifier = Modifier.size(164.dp), + modifier = Modifier + .height(164.dp) + .widthIn(max = 216.dp), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) } } @@ -292,6 +307,7 @@ private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) { .fillMaxWidth() .padding(horizontal = 20.dp), horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { title() @@ -310,7 +326,10 @@ private fun Charts( onItemClick: (MarketsListItemUM) -> Unit, modifier: Modifier = Modifier, ) { - BlockCard(modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { Column( modifier = Modifier .fillMaxWidth() @@ -380,9 +399,6 @@ private val LinearGradientSecondPart = Color(0xFFE05AED) @Composable private fun FeedListPreview() { TangemThemePreview { - FeedList( - state = createFeedPreviewState(), - onHeaderSizeChange = {}, - ) + FeedListContent(state = createFeedPreviewState()) } } \ 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/FeedListLoading.kt new file mode 100644 index 0000000000..69f94e61e6 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt @@ -0,0 +1,100 @@ +package com.tangem.features.feed.ui.feed + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.news.DefaultLoadingArticle +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.res.TangemThemePreview +import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder + +@Composable +internal fun MarketLoadingBlock() { + RectangleShimmer( + modifier = Modifier + .padding(start = 16.dp) + .size(width = 104.dp, height = 18.dp), + ) + SpacerH(12.dp) + ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp)) + SpacerH(32.dp) +} + +@Composable +internal fun MarketPulseLoadingBlock() { + RectangleShimmer() + SpacerH(8.dp) + LazyRow( + modifier = Modifier.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + state = rememberLazyListState(), + ) { + items(DEFAULT_CHART_SIZE_IN_MARKET) { + RectangleShimmer(modifier = Modifier.size(width = 124.dp, height = 36.dp)) + } + } + SpacerH(12.dp) + ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp)) + SpacerH(32.dp) +} + +@Composable +internal fun NewsLoadingBlock() { + Column { + RectangleShimmer() + SpacerH(12.dp) + TrendingLoadingArticle(modifier = Modifier.padding(horizontal = 16.dp)) + SpacerH(12.dp) + LazyRow( + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + state = rememberLazyListState(), + ) { + items(DEFAULT_CHART_SIZE_IN_MARKET) { + DefaultLoadingArticle() + } + } + } +} + +@Composable +private fun ChartsLoading(modifier: Modifier = Modifier) { + BlockCard(modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + repeat(DEFAULT_CHART_SIZE_IN_MARKET) { + MarketsListItemPlaceholder() + } + } + } +} + +private const val DEFAULT_CHART_SIZE_IN_MARKET = 5 + +@Preview(showBackground = true) +@Composable +private fun FeedListLoadingPreview() { + TangemThemePreview { + Column { + NewsLoadingBlock() + SpacerH(10.dp) + MarketLoadingBlock() + SpacerH(10.dp) + MarketPulseLoadingBlock() + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt index 9eaf7e5c4b..747d3d49bc 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt @@ -1,16 +1,15 @@ package com.tangem.features.feed.ui.feed.preview import com.tangem.common.ui.charts.state.MarketChartRawData -import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.feed.state.* import com.tangem.features.feed.ui.market.state.MarketsListItemUM import com.tangem.features.feed.ui.market.state.SortByTypeUM @@ -39,7 +38,7 @@ internal object FeedListPreviewDataProvider { onMarketItemClick = {}, onSortTypeClick = {}, ), - news = articles.filter { it.isTrending.not() }.toImmutableList(), + news = NewsUM.Content(articles.filter { it.isTrending.not() }.toImmutableList()), trendingArticle = articles.first { it.isTrending }, marketChartConfig = MarketChartConfig( marketCharts = createMarketCharts(marketItems, includeErrorState = false), @@ -165,7 +164,7 @@ internal object FeedListPreviewDataProvider { LabelUM( text = TextReference.Str("BTC"), leadingContent = LabelLeadingContentUM.Token( - icon = R.drawable.img_btc_22, + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/euro-coin.png", ), ), LabelUM(TextReference.Str("Regulation")), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt index 7e002be1f4..60f842ea9b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt @@ -14,7 +14,7 @@ internal data class FeedListUM( val currentDate: String, val searchBar: SearchBarUM, val feedListCallbacks: FeedListCallbacks, - val news: ImmutableList, + val news: NewsUM, val trendingArticle: ArticleConfigUM?, val marketChartConfig: MarketChartConfig, ) @@ -28,6 +28,12 @@ internal data class FeedListCallbacks( val onSortTypeClick: (SortByTypeUM) -> Unit, ) +@Immutable +internal sealed interface NewsUM { + data object Loading : NewsUM + data class Content(val content: ImmutableList) : NewsUM +} + internal data class MarketChartConfig( val marketCharts: ImmutableMap, val currentSortByType: SortByTypeUM = SortByTypeUM.TopGainers, @@ -49,7 +55,7 @@ internal sealed interface MarketChartUM { data class LoadingError(val onRetryClicked: () -> Unit) : MarketChartUM } -data class SortChartConfigUM( +internal data class SortChartConfigUM( val sortByType: SortByTypeUM, val isSelected: Boolean, ) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/SearchBarStateFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/SearchBarStateFactory.kt new file mode 100644 index 0000000000..bd33df97cc --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/SearchBarStateFactory.kt @@ -0,0 +1,24 @@ +package com.tangem.features.feed.ui.feed.state + +import com.tangem.utils.Provider + +internal class SearchBarStateFactory( + private val currentStateProvider: Provider, + private val onStateUpdate: (FeedListUM) -> Unit, +) { + + val searchQuery: String + get() = currentStateProvider().searchBar.query + + fun onSearchQueryChange(query: String) { + val currentState = currentStateProvider() + onStateUpdate( + currentState.copy( + searchBar = currentState.searchBar.copy( + query = query, + isActive = query.isNotEmpty(), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt new file mode 100644 index 0000000000..52bc5e2c2f --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/TrendingNewsStateFactory.kt @@ -0,0 +1,83 @@ +package com.tangem.features.feed.ui.feed.state + +import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.news.ShortArticle +import com.tangem.utils.Provider +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet + +internal class TrendingNewsStateFactory( + private val currentStateProvider: Provider, + private val onStateUpdate: (FeedListUM) -> Unit, +) { + + fun updateTrendingNewsState(news: List) { + val trendingArticleIndex = news.indexOfFirst { it.isTrending } + val trendingArticle = if (trendingArticleIndex != -1) news[trendingArticleIndex] else null + val commonArticles = if (trendingArticleIndex != -1) { + news.toMutableList().apply { removeAt(trendingArticleIndex) } + } else { + news + } + val currentState = currentStateProvider() + onStateUpdate( + currentState.copy( + trendingArticle = trendingArticle?.let { article -> + ArticleConfigUM( + id = article.id, + title = article.title, + score = article.score, + isTrending = true, + tags = article.categories.map { category -> + LabelUM(text = TextReference.Str(category.name)) + }.plus( + article.relatedTokens.map { token -> + LabelUM( + text = TextReference.Str(token.symbol), + leadingContent = LabelLeadingContentUM.Token( + iconUrl = getTokenIconUrlFromDefaultHost( + tokenId = CryptoCurrency.RawID(token.id), + ), + ), + ) + }, + ).toPersistentSet(), + createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY] + isViewed = article.viewed, + ) + }, + news = NewsUM.Content( + commonArticles.map { article -> + ArticleConfigUM( + id = article.id, + title = article.title, + score = article.score, + isTrending = false, + tags = article.categories.map { category -> + LabelUM(text = TextReference.Str(category.name)) + }.plus( + article.relatedTokens.map { token -> + LabelUM( + text = TextReference.Str(token.symbol), + leadingContent = LabelLeadingContentUM.Token( + iconUrl = getTokenIconUrlFromDefaultHost( + tokenId = CryptoCurrency.RawID(token.id), + ), + ), + ) + }, + ).toPersistentSet(), + createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY] + isViewed = article.viewed, + ) + }.toPersistentList(), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt index 58e368cc07..0136e2532f 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt @@ -6,10 +6,10 @@ import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.entry.BottomSheetState import kotlinx.serialization.Serializable @Stable diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt deleted file mode 100644 index 07aee6c5d9..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/BottomSheetState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.features.markets.entry - -enum class BottomSheetState { - EXPANDED, - COLLAPSED, -} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt index a30498b380..05a7e68670 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState @Stable interface MarketsEntryComponent { diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt index 5a5179c1ef..8062a9a1f6 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/tokenlist/MarketsTokenListComponent.kt @@ -7,10 +7,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.entry.BottomSheetState @Stable interface MarketsTokenListComponent : ComposableContentComponent { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt index b1f66a3c20..54aa223690 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt @@ -11,6 +11,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles @@ -21,7 +22,6 @@ import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalytics import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt index 06f3b26d19..9f441298d5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt @@ -15,10 +15,10 @@ import com.arkivanov.decompose.value.Value import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt index 9c72accb3a..a19e160096 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt @@ -15,10 +15,10 @@ import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.slide import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory import com.tangem.features.markets.tokenlist.MarketsTokenListComponent diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt index 8e6955c9b0..14e1c4dc72 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/DefaultMarketsTokenListComponent.kt @@ -16,12 +16,12 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute.MarketsTokenDetails.AnalyticsParams import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.markets.toSerializableParam -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.MarketsTokenListComponent import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel import com.tangem.features.markets.tokenlist.impl.ui.MarketsList diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt index 22574f445e..600133bc7c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/MarketsListModel.kt @@ -6,6 +6,7 @@ import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase @@ -17,7 +18,6 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.UserCountryError import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt index 041df48f96..5b80958c46 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/MarketsList.kt @@ -30,6 +30,7 @@ import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition @@ -46,7 +47,6 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.impl.R import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index ca7ea45cc6..81e2a852c5 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -124,6 +124,7 @@ dependencies { implementation(projects.features.tokenRecieve.api) implementation(projects.features.yieldSupply.api) implementation(projects.features.tangempay.details.api) + implementation(projects.features.feed.api) /** Common modules */ implementation(projects.common) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index da07a45435..32454e1a3f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -1,8 +1,9 @@ package com.tangem.feature.wallet.child.wallet -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot @@ -13,6 +14,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableDialogComponent @@ -23,6 +25,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogCon import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.features.biometry.AskBiometryComponent +import com.tangem.features.feed.entry.components.FeedEntryComponent +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams @@ -37,16 +41,25 @@ import kotlinx.coroutines.launch internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, + marketsEntryComponentFactory: MarketsEntryComponent.Factory, + feedEntryComponentFactory: FeedEntryComponent.Factory, private val renameWalletComponentFactory: RenameWalletComponent.Factory, - private val marketsEntryComponentFactory: MarketsEntryComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory, + private val feedFeatureToggle: FeedFeatureToggle, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: WalletModel = getOrCreateModel() + private val feedEntryComponent by lazy { + feedEntryComponentFactory.create(child("feedEntryComponent")) + } + private val marketsEntryComponent by lazy { + marketsEntryComponentFactory.create(child("marketsEntryComponent")) + } + init { lifecycle.subscribe(model.screenLifecycleProvider) doOnResume { model.onResume() } @@ -114,15 +127,23 @@ internal class WalletComponent @AssistedInject constructor( }, ) - private val marketsEntryComponent = marketsEntryComponentFactory.create(child("marketsEntryComponent")) - @Composable override fun Content(modifier: Modifier) { + val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } + var headerSize by remember { mutableStateOf(0.dp) } val dialog by dialog.subscribeAsState() WalletScreen( state = model.uiState.collectAsStateWithLifecycle().value, - marketsEntryComponent = marketsEntryComponent, + bottomSheetContent = { + BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = modifier, + ) + }, + bottomSheetHeaderHeightProvider = { headerSize }, + onBottomSheetStateChange = { bottomSheetState.value = it }, ) when (val dialog = dialog.child?.instance) { @@ -132,6 +153,27 @@ internal class WalletComponent @AssistedInject constructor( } } + @Composable + private fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier = Modifier, + ) { + if (feedFeatureToggle.isFeedEnabled) { + feedEntryComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) + } else { + marketsEntryComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) + } + } + @AssistedFactory interface Factory { fun create(appComponentContext: AppComponentContext, navigate: (WalletRoute) -> Unit): WalletComponent diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 586ccfc586..960d3c37f8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -42,6 +42,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.tangempay.TangemPayFeatureToggles @@ -99,6 +100,7 @@ internal class WalletModel @Inject constructor( private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val trackingContextProxy: TrackingContextProxy, + private val feedFeatureToggle: FeedFeatureToggle, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -121,6 +123,7 @@ internal class WalletModel @Inject constructor( analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy()) } + updateMarketToggle() suggestToOpenMarkets() maybeMigrateNames() @@ -143,6 +146,12 @@ internal class WalletModel @Inject constructor( } } + private fun updateMarketToggle() { + stateHolder.update { + it.copy(isNewMarketEnabled = feedFeatureToggle.isFeedEnabled) + } + } + private fun updateYieldSupplyApy() { if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) { modelScope.launch(dispatchers.default) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index c93c2cfd4d..e617d2d783 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -223,6 +223,7 @@ internal object WalletScreenPreviewData { isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsOnboarding = {}, + isNewMarketEnabled = false, ) internal val accountScreenState = diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index de8c2ee08a..572e187d6b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -110,6 +110,7 @@ internal class WalletStateController @Inject constructor() { isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsOnboarding = {}, + isNewMarketEnabled = false, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index df260b5856..1fb6a6d24c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -13,5 +13,6 @@ internal data class WalletScreenState( val event: StateEvent, val isHidingMode: Boolean, val showMarketsOnboarding: Boolean, + val isNewMarketEnabled: Boolean, val onDismissMarketsOnboarding: () -> Unit, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 16212dc865..1f2e8441c2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -51,6 +51,7 @@ import com.tangem.common.ui.expressStatus.expressTransactionsItems import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.components.rememberIsKeyboardVisible import com.tangem.core.ui.components.sheetscaffold.* @@ -87,15 +88,18 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPa import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDetailsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator -import com.tangem.features.markets.entry.BottomSheetState -import com.tangem.features.markets.entry.MarketsEntryComponent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.math.roundToInt @Composable -internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent) { +internal fun WalletScreen( + state: WalletScreenState, + bottomSheetContent: @Composable (() -> Unit), + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, +) { // It means that screen is still initializing if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return @@ -116,8 +120,10 @@ internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: Marke snackbarHostState = snackbarHostState, isAutoScroll = isAutoScroll, onAutoScrollReset = { isAutoScroll.value = false }, - marketsEntryComponent = marketsEntryComponent, + bottomSheetContent = bottomSheetContent, alertConfig = alertConfig, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, + onBottomSheetStateChange = onBottomSheetStateChange, ) WalletEventEffect( @@ -136,9 +142,11 @@ private fun WalletContent( walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, - marketsEntryComponent: MarketsEntryComponent, alertConfig: WalletAlertState?, onAutoScrollReset: () -> Unit, + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, + bottomSheetContent: @Composable (() -> Unit), ) { /* * Don't pass key to remember, because it will brake scroll animation. @@ -286,25 +294,15 @@ private fun WalletContent( ) } - val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } - - var headerSize by remember { mutableStateOf(0.dp) } - BaseScaffoldWithMarkets( state = state, listState = listState, selectedWallet = selectedWallet, snackbarHostState = snackbarHostState, - bottomSheetHeaderHeightProvider = { headerSize }, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, alertConfig = alertConfig, - onBottomSheetStateChange = { bottomSheetState.value = it }, - bottomSheetContent = { - marketsEntryComponent.BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = { headerSize = it }, - modifier = Modifier, - ) - }, + onBottomSheetStateChange = onBottomSheetStateChange, + bottomSheetContent = bottomSheetContent, content = scaffoldContent, ) } @@ -394,7 +392,11 @@ private inline fun BaseScaffoldWithMarkets( val maxHeight = LocalWindowSize.current.height val coroutineScope = rememberCoroutineScope() - val backgroundPrimary = TangemTheme.colors.background.primary + val background = if (state.isNewMarketEnabled) { + TangemTheme.colors.background.tertiary + } else { + TangemTheme.colors.background.primary + } val showMarketsHint by remember { derivedStateOf { @@ -407,7 +409,7 @@ private inline fun BaseScaffoldWithMarkets( } CompositionLocalProvider( - LocalMainBottomSheetColor provides remember { mutableStateOf(backgroundPrimary) }, + LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { val backgroundColor = LocalMainBottomSheetColor.current var isSearchFieldFocused by remember { mutableStateOf(false) } @@ -798,16 +800,11 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, - marketsEntryComponent = object : MarketsEntryComponent { - @Composable - override fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) { - Text("Markets Content") - } + bottomSheetContent = { + Text("Markets Content") }, + bottomSheetHeaderHeightProvider = { 10.dp }, + onBottomSheetStateChange = {}, ) } } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index 7011f2d423..bb9c96653e 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -3,11 +3,8 @@ package com.tangem.plugin.configuration.configurations.extension import com.android.build.gradle.BaseExtension import com.tangem.plugin.configuration.model.AppConfig import com.tangem.plugin.configuration.utils.findPlugin -import com.tangem.plugin.configuration.utils.findVersion import org.gradle.api.JavaVersion import org.gradle.api.Project -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.plugins internal fun BaseExtension.configureCompileSdk() { compileSdkVersion(AppConfig.compileSdkVersion) @@ -28,6 +25,7 @@ internal fun BaseExtension.configureCompose(project: Project) { contains(Regex(pattern = ":presentation\$")) || contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function + contains(Regex(pattern = ":features:feed:api\$")) || // provides Composable function contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function contains(Regex(pattern = ":features:txhistory:api\$")) || // provides Composable function contains(Regex(pattern = ":impl\$"))