From 6f959bcd3f214cc215848144eead50d983ffdedc Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Jan 2026 13:29:32 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 1 + .../models/OneTimePerSessionEvent.kt | 22 ++ .../com/tangem/core/analytics/Analytics.kt | 7 + .../data/news/DefaultNewsErrorResolver.kt | 22 ++ .../com/tangem/data/news/di/NewsDataModule.kt | 7 + .../news/repository/DefaultNewsRepository.kt | 9 +- .../tangem/domain/models/news/NewsError.kt | 20 +- .../tangem/domain/models/news/TrendingNews.kt | 2 +- .../tangem/domain/news/NewsErrorResolver.kt | 8 + features/feed/impl/build.gradle.kts | 1 + .../feed/model/feed/FeedComponentModel.kt | 288 +++++++++--------- .../feed/analytics/FeedAnalyticsEvent.kt | 94 ++++++ .../feed/state/FeedMarketsBatchFlowManager.kt | 18 +- .../model/feed/state/FeedStateController.kt | 70 +++++ .../transformers/FeedListUMTransformer.kt | 7 + .../UpdateGlobalFeedStateTransformer.kt | 76 +++++ .../UpdateMarketChartsTransformer.kt | 102 +++++++ .../UpdateTrendingNewsStateTransformer.kt} | 66 ++-- .../tangem/features/feed/ui/feed/FeedList.kt | 25 +- .../preview/FeedListPreviewDataProvider.kt | 2 + .../features/feed/ui/feed/state/FeedListUM.kt | 6 +- 21 files changed, 641 insertions(+), 212 deletions(-) create mode 100644 core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt create mode 100644 data/news/src/main/java/com/tangem/data/news/DefaultNewsErrorResolver.kt create mode 100644 domain/news/src/main/java/com/tangem/domain/news/NewsErrorResolver.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/FeedListUMTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateMarketChartsTransformer.kt rename features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/{TrendingNewsStateFactory.kt => transformers/UpdateTrendingNewsStateTransformer.kt} (54%) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 9488b9c840..fae4659e84 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -78,6 +78,7 @@ sealed class AnalyticsParam { data object Onboarding : ScreensSources("Onboarding") data object LongTap : ScreensSources("Long Tap") data object Markets : ScreensSources("Markets") + data object MarketPulse : ScreensSources("Market Pulse") data object TangemPay : ScreensSources("Tangem Pay") data object WalletSettings : ScreensSources("Wallet Settings") data object Upgrade : ScreensSources("Upgrade") diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt new file mode 100644 index 0000000000..a94df878eb --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt @@ -0,0 +1,22 @@ +package com.tangem.core.analytics.models + +/** + * Marker interface for analytics events that should be sent only once per session. + * + * Events implementing this interface will be tracked by their [oneTimeEventId] to ensure + * they are not sent multiple times during the same application session. Once an event + * with a specific [oneTimeEventId] has been sent, subsequent attempts to send an event + * with the same ID will be ignored. + * + * @see Analytics.send + */ +interface OneTimePerSessionEvent { + /** + * Unique identifier for the one-time event. + * + * This ID is used to track whether the event has already been sent in the current session. + * Events with the same [oneTimeEventId] will only be sent once, even if they are + * different instances of the same event class. + */ + val oneTimeEventId: String +} \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index 9703947954..c5630c4906 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -6,6 +6,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.core.analytics.api.* import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.ExceptionAnalyticsEvent +import com.tangem.core.analytics.models.OneTimePerSessionEvent import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex @@ -31,6 +32,7 @@ object Analytics : GlobalAnalyticsEventHandler { private val handlers = mutableMapOf() private val paramsInterceptors = ConcurrentHashMap() + private val oneEventsPerSession = ConcurrentHashMap() private val analyticsFilters = mutableSetOf() private val analyticsMutex = Mutex() @@ -85,6 +87,11 @@ object Analytics : GlobalAnalyticsEventHandler { override fun send(event: AnalyticsEvent) { analyticsScope.launch { + if (event is OneTimePerSessionEvent && + oneEventsPerSession.putIfAbsent(event.oneTimeEventId, true) != null + ) { + return@launch + } event.params = applyParamsInterceptors(event) val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) } diff --git a/data/news/src/main/java/com/tangem/data/news/DefaultNewsErrorResolver.kt b/data/news/src/main/java/com/tangem/data/news/DefaultNewsErrorResolver.kt new file mode 100644 index 0000000000..3d35ed1f4a --- /dev/null +++ b/data/news/src/main/java/com/tangem/data/news/DefaultNewsErrorResolver.kt @@ -0,0 +1,22 @@ +package com.tangem.data.news + +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.domain.models.news.NewsError +import com.tangem.domain.news.NewsErrorResolver + +internal class DefaultNewsErrorResolver : NewsErrorResolver { + + override fun resolve(throwable: Throwable?): NewsError { + return when (throwable) { + is ApiResponseError.HttpException -> { + NewsError.HttpError( + code = throwable.code.numericCode, + message = throwable.message.orEmpty(), + ) + } + else -> { + NewsError.NotHttpError + } + } + } +} \ No newline at end of file diff --git a/data/news/src/main/java/com/tangem/data/news/di/NewsDataModule.kt b/data/news/src/main/java/com/tangem/data/news/di/NewsDataModule.kt index cb558a2283..90851234d3 100644 --- a/data/news/src/main/java/com/tangem/data/news/di/NewsDataModule.kt +++ b/data/news/src/main/java/com/tangem/data/news/di/NewsDataModule.kt @@ -1,10 +1,12 @@ package com.tangem.data.news.di +import com.tangem.data.news.DefaultNewsErrorResolver import com.tangem.data.news.repository.DefaultNewsRepository import com.tangem.datasource.api.news.NewsApi import com.tangem.datasource.local.news.details.NewsDetailsStore import com.tangem.datasource.local.news.trending.TrendingNewsStore import com.tangem.datasource.local.news.viewed.NewsViewedStore +import com.tangem.domain.news.NewsErrorResolver import com.tangem.domain.news.repository.NewsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -25,6 +27,7 @@ internal object NewsDataModule { newsDetailsStore: NewsDetailsStore, trendingNewsStore: TrendingNewsStore, newsViewedStore: NewsViewedStore, + newsErrorResolver: NewsErrorResolver, ): NewsRepository { return DefaultNewsRepository( newsApi = newsApi, @@ -32,6 +35,10 @@ internal object NewsDataModule { newsDetailsStore = newsDetailsStore, trendingNewsStore = trendingNewsStore, newsViewedStore = newsViewedStore, + newsErrorResolver = newsErrorResolver, ) } + + @Provides + fun provideNewsErrorResolver(): NewsErrorResolver = DefaultNewsErrorResolver() } \ No newline at end of file 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 f503535500..8466016a58 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 @@ -9,6 +9,7 @@ import com.tangem.datasource.local.news.details.NewsDetailsStore import com.tangem.datasource.local.news.trending.TrendingNewsStore import com.tangem.datasource.local.news.viewed.NewsViewedStore import com.tangem.domain.models.news.* +import com.tangem.domain.news.NewsErrorResolver import com.tangem.domain.news.model.NewsListBatchFlow import com.tangem.domain.news.model.NewsListBatchingContext import com.tangem.domain.news.model.NewsListConfig @@ -32,6 +33,7 @@ internal class DefaultNewsRepository( private val newsDetailsStore: NewsDetailsStore, private val trendingNewsStore: TrendingNewsStore, private val newsViewedStore: NewsViewedStore, + private val newsErrorResolver: NewsErrorResolver, ) : NewsRepository { override fun getNewsListBatchFlow(context: NewsListBatchingContext, batchSize: Int): NewsListBatchFlow { @@ -176,12 +178,7 @@ internal class DefaultNewsRepository( trendingNewsStore.clear() trendingNewsStore.store( key = TRENDING_NEWS_KEY, - value = TrendingNews.Error( - NewsError.Unknown( - message = apiResponse.cause.message, - code = null, - ), - ), + value = TrendingNews.Error(error = newsErrorResolver.resolve(apiResponse.cause.cause)), ) } is ApiResponse.Success -> { diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/news/NewsError.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/news/NewsError.kt index 5055eacbbf..f92b62ca6f 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/news/NewsError.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/news/NewsError.kt @@ -1,19 +1,11 @@ package com.tangem.domain.models.news -import kotlinx.serialization.Serializable +sealed interface NewsError { -@Serializable -sealed class NewsError { - abstract val message: String? - abstract val code: Int? + data class HttpError( + val code: Int, + val message: String, + ) : NewsError - data class ArticleNotFound( - override val message: String?, - override val code: Int?, - ) : NewsError() - - data class Unknown( - override val message: String?, - override val code: Int?, - ) : NewsError() + data object NotHttpError : NewsError } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/news/TrendingNews.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/news/TrendingNews.kt index d3b0bbdf87..ec4a742686 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/news/TrendingNews.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/news/TrendingNews.kt @@ -12,5 +12,5 @@ sealed interface TrendingNews { data class Data(val articles: List) : TrendingNews @Serializable - data class Error(val throwable: NewsError) : TrendingNews + data class Error(val error: NewsError) : TrendingNews } \ No newline at end of file diff --git a/domain/news/src/main/java/com/tangem/domain/news/NewsErrorResolver.kt b/domain/news/src/main/java/com/tangem/domain/news/NewsErrorResolver.kt new file mode 100644 index 0000000000..d4ece74780 --- /dev/null +++ b/domain/news/src/main/java/com/tangem/domain/news/NewsErrorResolver.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.news + +import com.tangem.domain.models.news.NewsError + +interface NewsErrorResolver { + + fun resolve(throwable: Throwable?): NewsError +} \ No newline at end of file diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index ed4e7f4ba6..e16f11c113 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -90,6 +90,7 @@ dependencies { implementation(projects.core.analytics.models) implementation(projects.core.navigation) implementation(projects.core.utils) + implementation(projects.core.datasource) /* Common */ implementation(projects.common.ui) 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 index f9c544b6e0..c8d8412ae3 100644 --- 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 @@ -2,6 +2,9 @@ package com.tangem.features.feed.model.feed import androidx.compose.runtime.Stable import arrow.core.getOrElse +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -16,13 +19,16 @@ import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase import com.tangem.features.feed.components.feed.DefaultFeedComponent import com.tangem.features.feed.impl.R +import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent import com.tangem.features.feed.model.feed.state.FeedMarketsBatchFlowManager -import com.tangem.features.feed.model.feed.state.TrendingNewsStateFactory +import com.tangem.features.feed.model.feed.state.FeedStateController +import com.tangem.features.feed.model.feed.state.transformers.UpdateGlobalFeedStateTransformer +import com.tangem.features.feed.model.feed.state.transformers.UpdateMarketChartsTransformer +import com.tangem.features.feed.model.feed.state.transformers.UpdateTrendingNewsStateTransformer import com.tangem.features.feed.model.market.list.state.SortByTypeUM import com.tangem.features.feed.ui.feed.state.* import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentHashMap import kotlinx.coroutines.Job @@ -32,18 +38,19 @@ import kotlinx.coroutines.launch import org.joda.time.DateTime import org.joda.time.DateTimeZone import javax.inject.Inject -import kotlin.collections.all -import kotlin.collections.map @Stable @ModelScoped +@Suppress("LongParameterList") internal class FeedComponentModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase, private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, paramsContainer: ParamsContainer, + private val stateController: FeedStateController, ) : Model() { private val params = paramsContainer.require() @@ -66,23 +73,12 @@ internal class FeedComponentModel @Inject constructor( ) internal val state: StateFlow - field = MutableStateFlow(initialState()) - - private val trendingNewsStateFactory by lazy(LazyThreadSafetyMode.NONE) { - TrendingNewsStateFactory( - currentStateProvider = Provider { state.value }, - onStateUpdate = { newState -> state.update { newState } }, - onRetryClicked = { - modelScope.launch(dispatchers.default) { - fetchTrendingNewsUseCase.invoke() - } - }, - ) - } + get() = stateController.uiState val isVisibleOnScreen = MutableStateFlow(false) init { + initializeState() updateCallbacks() modelScope.launch(dispatchers.default) { @@ -96,10 +92,50 @@ internal class FeedComponentModel @Inject constructor( flow3 = marketsBatchFlowManager.errorStatesByOrder, flow4 = manageTrendingNewsUseCase.observeTrendingNews(), ) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult -> - updateMarketCharts(itemsByOrder, loadingStatesByOrder, errorStatesByOrder) - trendingNewsStateFactory.updateTrendingNewsState(result = trendingNewsResult) - updateGlobalState() - val currentSortType = state.value.marketChartConfig.currentSortByType + val globalStateTransformer = UpdateGlobalFeedStateTransformer( + loadingStatesByOrder = loadingStatesByOrder, + errorStatesByOrder = errorStatesByOrder, + trendingNewsResult = trendingNewsResult, + onRetryClicked = { + modelScope.launch(dispatchers.default) { + stateController.update { currentState -> + currentState.copy(globalState = GlobalFeedState.Loading) + } + fetchTrendingNewsUseCase.invoke() + marketsBatchFlowManager.reloadAll() + } + }, + analyticsEventHandler = analyticsEventHandler, + ) + + val currentState = stateController.value + val newGlobalState = globalStateTransformer.transform(currentState) + val shouldSkipIndividualStates = newGlobalState.globalState !is GlobalFeedState.Content + + stateController.update(globalStateTransformer) + + if (!shouldSkipIndividualStates) { + stateController.updateAll( + UpdateMarketChartsTransformer( + itemsByOrder = itemsByOrder, + loadingStatesByOrder = loadingStatesByOrder, + errorStatesByOrder = errorStatesByOrder, + onReloadAll = { marketsBatchFlowManager.reloadAll() }, + analyticsEventHandler = analyticsEventHandler, + ), + UpdateTrendingNewsStateTransformer( + result = trendingNewsResult, + onRetryClicked = { + modelScope.launch(dispatchers.default) { + fetchTrendingNewsUseCase.invoke() + } + }, + analyticsEventHandler = analyticsEventHandler, + ), + ) + } + + val currentSortType = stateController.value.marketChartConfig.currentSortByType val items = itemsByOrder[currentSortType] val isLoading = loadingStatesByOrder[currentSortType] == true if (items != null && items.isNotEmpty() && !isLoading) { @@ -127,6 +163,10 @@ internal class FeedComponentModel @Inject constructor( } } + private fun initializeState() { + stateController.update { initialState() } + } + private fun initialState(): FeedListUM { return FeedListUM( currentDate = getCurrentDate(), @@ -141,6 +181,12 @@ internal class FeedComponentModel @Inject constructor( onOpenAllNews = {}, onMarketItemClick = {}, onSortTypeClick = {}, + onSliderScroll = { + analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselScrolled()) + }, + onSliderEndReached = { + analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselEndReached()) + }, ), news = NewsUM( content = persistentListOf(), @@ -165,101 +211,8 @@ internal class FeedComponentModel @Inject constructor( return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate) } - private fun updateMarketCharts( - itemsByOrder: Map>, - loadingStatesByOrder: Map, - errorStatesByOrder: Map, - ) { - state.update { currentState -> - val newMarketCharts = buildMap { - SortByTypeUM.entries.forEach { sortByType -> - val items = itemsByOrder[sortByType] ?: persistentListOf() - val isLoading = loadingStatesByOrder[sortByType] == true - val hasError = errorStatesByOrder[sortByType] == true - - when { - hasError -> { - put( - sortByType, - MarketChartUM.LoadingError( - onRetryClicked = { - marketsBatchFlowManager.reloadAll() - }, - ), - ) - } - isLoading -> { - put(sortByType, MarketChartUM.Loading) - } - items.isEmpty() -> { - put( - sortByType, - MarketChartUM.LoadingError( - onRetryClicked = { - marketsBatchFlowManager.reloadAll() - }, - ), - ) - } - else -> { - put( - sortByType, - MarketChartUM.Content( - items = items, - sortChartConfig = SortChartConfigUM( - sortByType = sortByType, - isSelected = sortByType == currentState.marketChartConfig.currentSortByType, - ), - ), - ) - } - } - } - }.toPersistentHashMap() - - currentState.copy( - marketChartConfig = currentState.marketChartConfig.copy( - marketCharts = newMarketCharts, - ), - ) - } - } - - private fun updateGlobalState() { - state.update { currentState -> - val newsState = currentState.news - val marketCharts = currentState.marketChartConfig.marketCharts - - val isNewsLoading = newsState.newsUMState == NewsUMState.LOADING - val areAllChartsLoading = marketCharts.values.all { it is MarketChartUM.Loading } - - val isNewsError = newsState.newsUMState == NewsUMState.ERROR - val areAllChartsError = marketCharts.values.all { it is MarketChartUM.LoadingError } - - val newGlobalState = when { - isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading - isNewsError && areAllChartsError -> GlobalFeedState.Error( - onRetryClicked = { - modelScope.launch(dispatchers.default) { - fetchTrendingNewsUseCase.invoke() - marketsBatchFlowManager.reloadAll() - } - }, - ) - else -> GlobalFeedState.Content - } - - val currentGlobalState = currentState.globalState - if (currentGlobalState::class != newGlobalState::class) { - currentState.copy(globalState = newGlobalState) - } else { - currentState - } - } - } - - private fun onSortTypeClick(sortByType: SortByTypeUM) { - state.update { currentState -> + private fun handleSortTypeClicked(sortByType: SortByTypeUM) { + stateController.update { currentState -> val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) -> when (chart) { is MarketChartUM.Content -> { @@ -297,44 +250,85 @@ internal class FeedComponentModel @Inject constructor( } private fun updateCallbacks() { - state.update { feedListUM -> + stateController.update { feedListUM -> feedListUM.copy( feedListCallbacks = feedListUM.feedListCallbacks.copy( - onSortTypeClick = ::onSortTypeClick, - onMarketItemClick = { item -> - val tokenMarket = marketsBatchFlowManager.getTokenMarketById(item.id) - if (tokenMarket != null) { - params.feedClickIntents.onMarketItemClick( - token = tokenMarket.toSerializableParam(), - appCurrency = currentAppCurrency.value, - ) - } - }, - onMarketOpenClick = { sortBy -> - params.feedClickIntents.onMarketOpenClick(sortBy) - }, - onArticleClick = { articleId -> - val trendingArticleId = state.value.trendingArticle?.id - params.feedClickIntents.onArticleClick( - articleId = articleId, - preselectedArticlesId = listOfNotNull(trendingArticleId) + - when (state.value.news.newsUMState) { - NewsUMState.CONTENT -> state.value.news.content.map { it.id } - NewsUMState.LOADING, - NewsUMState.ERROR, - -> emptyList() - }, - paginationConfig = null, - ) - }, - onOpenAllNews = { - params.feedClickIntents.onOpenAllNews() - }, + onSortTypeClick = ::handleSortTypeClicked, + onMarketItemClick = ::handleMarketItemClicked, + onMarketOpenClick = ::handleMarketOpenClicked, + onArticleClick = ::handleArticleClicked, + onOpenAllNews = ::handleOpenAllNews, ), ) } } + private fun handleMarketItemClicked(item: MarketsListItemUM) { + val tokenMarket = marketsBatchFlowManager.getTokenMarketById(item.id) + if (tokenMarket != null) { + params.feedClickIntents.onMarketItemClick( + token = tokenMarket.toSerializableParam(), + appCurrency = currentAppCurrency.value, + ) + } + } + + private fun handleMarketOpenClicked(sortBy: SortByTypeUM) { + when (sortBy) { + SortByTypeUM.Rating -> { + analyticsEventHandler.send( + FeedAnalyticsEvent.TokenListOpened( + AnalyticsParam.ScreensSources.Markets, + ), + ) + } + SortByTypeUM.Trending, + SortByTypeUM.ExperiencedBuyers, + SortByTypeUM.TopGainers, + SortByTypeUM.TopLosers, + SortByTypeUM.Staking, + SortByTypeUM.YieldSupply, + -> { + analyticsEventHandler.send( + FeedAnalyticsEvent.TokenListOpened( + AnalyticsParam.ScreensSources.MarketPulse, + ), + ) + } + } + params.feedClickIntents.onMarketOpenClick(sortBy) + } + + private fun handleArticleClicked(articleId: Int) { + val currentState = stateController.value + val trendingArticleId = currentState.trendingArticle?.id + if (articleId == trendingArticleId) { + analyticsEventHandler.send( + event = FeedAnalyticsEvent.NewsCarouselTrendingClicked(newsId = articleId), + ) + } + params.feedClickIntents.onArticleClick( + articleId = articleId, + preselectedArticlesId = listOfNotNull(trendingArticleId) + + when (currentState.news.newsUMState) { + NewsUMState.CONTENT -> currentState.news.content.map { it.id } + NewsUMState.LOADING, + NewsUMState.ERROR, + -> emptyList() + }, + paginationConfig = null, + ) + } + + private fun handleOpenAllNews(isFromCarouselButton: Boolean) { + if (isFromCarouselButton) { + analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselAllNewsButton()) + } else { + analyticsEventHandler.send(FeedAnalyticsEvent.NewsListOpened()) + } + params.feedClickIntents.onOpenAllNews() + } + private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order { return when (this) { SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt new file mode 100644 index 0000000000..c61c143dd9 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt @@ -0,0 +1,94 @@ +package com.tangem.features.feed.model.feed.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE +import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE +import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE +import com.tangem.core.analytics.models.OneTimePerSessionEvent + +internal sealed class FeedAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = "Markets", event = event, params = params) { + + data class TokenListOpened( + private val screensSources: AnalyticsParam.ScreensSources, + ) : FeedAnalyticsEvent( + event = "Token List Opened", + params = mapOf( + SOURCE to screensSources.value, + ), + ) + + class NewsListOpened : FeedAnalyticsEvent( + event = "News List Opened", + params = mapOf( + SOURCE to AnalyticsParam.ScreensSources.Markets.value, + ), + ) + + data class NewsCarouselTrendingClicked(private val newsId: Int) : FeedAnalyticsEvent( + event = "News Carousel Trending Clicked", + params = mapOf( + "News id" to newsId.toString(), + ), + ), OneTimePerSessionEvent { + override val oneTimeEventId: String = event + } + + class NewsCarouselAllNewsButton : FeedAnalyticsEvent( + event = "News Carousel All News button", + ), OneTimePerSessionEvent { + override val oneTimeEventId: String = event + } + + class NewsCarouselEndReached : FeedAnalyticsEvent( + event = "News Carousel End Reached", + ), OneTimePerSessionEvent { + override val oneTimeEventId: String = event + } + + class NewsCarouselScrolled : FeedAnalyticsEvent( + event = "News Carousel Scrolled", + ), OneTimePerSessionEvent { + override val oneTimeEventId: String = event + } + + data class MarketsLoadError( + private val code: Int?, + private val message: String, + ) : FeedAnalyticsEvent( + event = "Markets Load Error", + params = mapOf( + ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(), + ERROR_MESSAGE to message, + ), + ) + + data class NewsLoadError( + private val code: Int?, + private val message: String, + ) : FeedAnalyticsEvent( + event = "News Load Error", + params = mapOf( + ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(), + ERROR_MESSAGE to message, + ), + ) + + data class AllWidgetsLoadError( + private val code: Int?, + private val message: String, + ) : FeedAnalyticsEvent( + event = "All Widgets Load Error", + params = mapOf( + ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(), + ERROR_MESSAGE to message, + ), + ) + + private companion object { + const val IS_NOT_HTTP_ERROR = "Is not http error" + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedMarketsBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedMarketsBatchFlowManager.kt index 026527248e..ac4f510e63 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedMarketsBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedMarketsBatchFlowManager.kt @@ -66,15 +66,15 @@ internal class FeedMarketsBatchFlowManager( initialValue = emptyMap(), ) - val errorStatesByOrder: StateFlow> = + val errorStatesByOrder: StateFlow> = combine( TokenMarketListConfig.Order.entries.mapNotNull { order -> - managersByOrder[order]?.hasError?.map { hasError -> order to hasError } + managersByOrder[order]?.error?.map { error -> order to error } }, ) { errorStatesList -> - errorStatesList.associate { (order, hasError) -> + errorStatesList.associate { (order, error) -> val sortByType = order.toSortByTypeUM() - sortByType to hasError + sortByType to error } }.stateIn( scope = modelScope, @@ -194,19 +194,19 @@ internal class FeedMarketsBatchFlowManager( initialValue = false, ) - val hasError = batchFlow.state + val error = batchFlow.state .map { state -> when (val status = state.status) { - is PaginationStatus.InitialLoadingError -> true - is PaginationStatus.Paginating -> status.lastResult is BatchFetchResult.Error - else -> false + is PaginationStatus.InitialLoadingError -> status.throwable + is PaginationStatus.Paginating -> (status.lastResult as? BatchFetchResult.Error)?.throwable + else -> null } } .distinctUntilChanged() .stateIn( scope = modelScope, started = Eagerly, - initialValue = false, + initialValue = null, ) val onLastBatchLoadedSuccess = batchFlow.state diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt new file mode 100644 index 0000000000..161b83222b --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt @@ -0,0 +1,70 @@ +package com.tangem.features.feed.model.feed.state + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.features.feed.model.feed.state.transformers.FeedListUMTransformer +import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.feed.state.* +import kotlinx.collections.immutable.persistentHashMapOf +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@ModelScoped +internal class FeedStateController @Inject constructor() { + + private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) + + val uiState: StateFlow get() = mutableUiState.asStateFlow() + + val value: FeedListUM get() = uiState.value + + fun update(function: (FeedListUM) -> FeedListUM) { + mutableUiState.update(function = function) + } + + fun update(transformer: FeedListUMTransformer) { + mutableUiState.update(function = transformer::transform) + } + + fun updateAll(vararg transformers: FeedListUMTransformer) { + transformers.forEach { mutableUiState.update(function = it::transform) } + } + + fun clear() { + mutableUiState.update { getInitialState() } + } + + private fun getInitialState(): FeedListUM { + return FeedListUM( + currentDate = "", + feedListSearchBar = FeedListSearchBar( + placeholderText = com.tangem.core.ui.extensions.TextReference.EMPTY, + onBarClick = {}, + ), + feedListCallbacks = FeedListCallbacks( + onSearchClick = {}, + onMarketOpenClick = {}, + onArticleClick = {}, + onOpenAllNews = {}, + onMarketItemClick = {}, + onSortTypeClick = {}, + onSliderScroll = {}, + onSliderEndReached = {}, + ), + news = NewsUM( + content = persistentListOf(), + onRetryClicked = {}, + newsUMState = NewsUMState.LOADING, + ), + trendingArticle = null, + marketChartConfig = MarketChartConfig( + marketCharts = persistentHashMapOf(), + currentSortByType = SortByTypeUM.Trending, + ), + globalState = GlobalFeedState.Loading, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/FeedListUMTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/FeedListUMTransformer.kt new file mode 100644 index 0000000000..953e0af7d6 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/FeedListUMTransformer.kt @@ -0,0 +1,7 @@ +package com.tangem.features.feed.model.feed.state.transformers + +import com.tangem.features.feed.ui.feed.state.FeedListUM + +internal interface FeedListUMTransformer { + fun transform(prevState: FeedListUM): FeedListUM +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt new file mode 100644 index 0000000000..ee8c1226fe --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt @@ -0,0 +1,76 @@ +package com.tangem.features.feed.model.feed.state.transformers + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.domain.models.news.TrendingNews +import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent +import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.feed.state.FeedListUM +import com.tangem.features.feed.ui.feed.state.GlobalFeedState +import com.tangem.features.feed.ui.feed.state.NewsUMState + +internal class UpdateGlobalFeedStateTransformer( + private val loadingStatesByOrder: Map, + private val errorStatesByOrder: Map, + private val trendingNewsResult: TrendingNews, + private val onRetryClicked: () -> Unit, + private val analyticsEventHandler: AnalyticsEventHandler, +) : FeedListUMTransformer { + + @Suppress("CyclomaticComplexMethod") + override fun transform(prevState: FeedListUM): FeedListUM { + val previousGlobalState = prevState.globalState + + val isNewsLoading = prevState.news.newsUMState == NewsUMState.LOADING + val isNewsError = trendingNewsResult is TrendingNews.Error || + prevState.news.newsUMState == NewsUMState.ERROR + + val hasLoadingInCharts = loadingStatesByOrder.values.any { it } + val hasErrorInCharts = errorStatesByOrder.values.any { it != null } + val hasAllErrorInCharts = errorStatesByOrder.values.isNotEmpty() && errorStatesByOrder.values.all { it != null } + + val areAllChartsLoading = loadingStatesByOrder.values.isNotEmpty() && loadingStatesByOrder.values.all { it } + val areAllChartsError = hasAllErrorInCharts || hasErrorInCharts && hasLoadingInCharts + + val newGlobalState = when { + isNewsLoading && areAllChartsError -> GlobalFeedState.Loading + isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading + isNewsError && areAllChartsLoading -> GlobalFeedState.Loading + isNewsError && areAllChartsError -> { + if (previousGlobalState !is GlobalFeedState.Error) { + sendErrorAnalytics(errorStatesByOrder) + } + GlobalFeedState.Error( + onRetryClicked = onRetryClicked, + ) + } + else -> GlobalFeedState.Content + } + + return prevState.copy( + globalState = newGlobalState, + news = prevState.news.copy( + newsUMState = when { + isNewsError -> NewsUMState.ERROR + else -> NewsUMState.CONTENT + }, + ), + ) + } + + private fun sendErrorAnalytics(errorStatesByOrder: Map) { + val firstError: Throwable? = errorStatesByOrder.values.firstNotNullOfOrNull { it } + firstError?.let { error -> + val (code, message) = when (error) { + is ApiResponseError.HttpException -> error.code.numericCode to error.message + else -> null to "" + } + analyticsEventHandler.send( + FeedAnalyticsEvent.AllWidgetsLoadError( + code = code, + message = message.orEmpty(), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateMarketChartsTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateMarketChartsTransformer.kt new file mode 100644 index 0000000000..d556835e7a --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateMarketChartsTransformer.kt @@ -0,0 +1,102 @@ +package com.tangem.features.feed.model.feed.state.transformers + +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent +import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.feed.state.FeedListUM +import com.tangem.features.feed.ui.feed.state.MarketChartUM +import com.tangem.features.feed.ui.feed.state.SortChartConfigUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentHashMap + +internal class UpdateMarketChartsTransformer( + private val itemsByOrder: Map>, + private val loadingStatesByOrder: Map, + private val errorStatesByOrder: Map, + private val onReloadAll: () -> Unit, + private val analyticsEventHandler: AnalyticsEventHandler, +) : FeedListUMTransformer { + + override fun transform(prevState: FeedListUM): FeedListUM { + val newMarketCharts = buildMap { + SortByTypeUM.entries.forEach { sortByType -> + val items = itemsByOrder[sortByType] ?: persistentListOf() + val isLoading = loadingStatesByOrder[sortByType] == true + val hasError = errorStatesByOrder[sortByType] != null + + when { + hasError -> { + put( + sortByType, + MarketChartUM.LoadingError( + onRetryClicked = onReloadAll, + ), + ) + } + isLoading -> { + put(sortByType, MarketChartUM.Loading) + } + items.isEmpty() -> { + put( + sortByType, + MarketChartUM.LoadingError( + onRetryClicked = onReloadAll, + ), + ) + } + else -> { + put( + sortByType, + MarketChartUM.Content( + items = items, + sortChartConfig = SortChartConfigUM( + sortByType = sortByType, + isSelected = sortByType == prevState.marketChartConfig.currentSortByType, + ), + ), + ) + } + } + } + }.toPersistentHashMap() + + val wasAnyErrorBefore = prevState.marketChartConfig.marketCharts.values.any { + it is MarketChartUM.LoadingError + } + val isAnyErrorNow = newMarketCharts.values.any { it is MarketChartUM.LoadingError } + + if (!wasAnyErrorBefore && isAnyErrorNow) { + sendErrorAnalytics(errorStatesByOrder, itemsByOrder) + } + + return prevState.copy( + marketChartConfig = prevState.marketChartConfig.copy( + marketCharts = newMarketCharts, + ), + ) + } + + private fun sendErrorAnalytics( + errorStatesByOrder: Map, + itemsByOrder: Map>, + ) { + val firstError = errorStatesByOrder.values.firstNotNullOfOrNull { it } + if (firstError == null && !itemsByOrder.values.any { it.isEmpty() }) { + return + } + val (code, message) = if (firstError is ApiResponseError.HttpException) { + firstError.code.numericCode to firstError.message.orEmpty() + } else { + null to "" + } + analyticsEventHandler.send( + FeedAnalyticsEvent.MarketsLoadError( + code = code, + message = message, + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/TrendingNewsStateFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateTrendingNewsStateTransformer.kt similarity index 54% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/TrendingNewsStateFactory.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateTrendingNewsStateTransformer.kt index a132fd706f..5b19b9c5a9 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/TrendingNewsStateFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateTrendingNewsStateTransformer.kt @@ -1,8 +1,11 @@ -package com.tangem.features.feed.model.feed.state +package com.tangem.features.feed.model.feed.state.transformers +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.models.news.NewsError import com.tangem.domain.models.news.ShortArticle import com.tangem.domain.models.news.TrendingNews import com.tangem.features.feed.model.converter.ShortArticleToArticleConfigUMConverter +import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent import com.tangem.features.feed.ui.feed.state.FeedListUM import com.tangem.features.feed.ui.feed.state.NewsUM import com.tangem.features.feed.ui.feed.state.NewsUMState @@ -10,21 +13,32 @@ import com.tangem.utils.Provider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -internal class TrendingNewsStateFactory( - private val currentStateProvider: Provider, - private val onStateUpdate: (FeedListUM) -> Unit, +internal class UpdateTrendingNewsStateTransformer( + private val result: TrendingNews, private val onRetryClicked: () -> Unit, -) { + private val analyticsEventHandler: AnalyticsEventHandler, +) : FeedListUMTransformer { - fun updateTrendingNewsState(result: TrendingNews) { - val currentState = currentStateProvider() - when (result) { - is TrendingNews.Data -> handleDataState(currentState, result.articles) - is TrendingNews.Error -> handleErrorState(currentState) + override fun transform(prevState: FeedListUM): FeedListUM { + return when (result) { + is TrendingNews.Data -> handleDataState(prevState, result.articles) + is TrendingNews.Error -> { + val (code, message) = when (val error = result.error) { + is NewsError.HttpError -> error.code to error.message + NewsError.NotHttpError -> null to "" + } + analyticsEventHandler.send( + FeedAnalyticsEvent.NewsLoadError( + code = code, + message = message, + ), + ) + handleErrorState(prevState) + } } } - private fun handleDataState(currentState: FeedListUM, articles: List) { + private fun handleDataState(currentState: FeedListUM, articles: List): FeedListUM { val (trendingArticle, commonArticles) = separateTrendingAndCommonArticles(articles) val commonArticlesUM = getShortArticleConfigConverter(isTrending = false) .convert(commonArticles) @@ -40,26 +54,22 @@ internal class TrendingNewsStateFactory( ) } - onStateUpdate( - currentState.copy( - trendingArticle = trendingArticle?.let { article -> - getShortArticleConfigConverter(isTrending = true) - .convert(listOf(article)) - }?.firstOrNull(), - news = updatedNews, - ), + return currentState.copy( + trendingArticle = trendingArticle?.let { article -> + getShortArticleConfigConverter(isTrending = true) + .convert(listOf(article)) + }?.firstOrNull(), + news = updatedNews, ) } - private fun handleErrorState(currentState: FeedListUM) { - onStateUpdate( - currentState.copy( - trendingArticle = null, - news = NewsUM( - content = persistentListOf(), - onRetryClicked = onRetryClicked, - newsUMState = NewsUMState.ERROR, - ), + private fun handleErrorState(currentState: FeedListUM): FeedListUM { + return currentState.copy( + trendingArticle = null, + news = NewsUM( + content = persistentListOf(), + onRetryClicked = onRetryClicked, + newsUMState = NewsUMState.ERROR, ), ) } 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 e402b48dff..f25acdab2d 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 @@ -19,6 +19,7 @@ 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.res.vectorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -329,7 +330,7 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, ) } }, - onSeeAllClick = feedListCallbacks.onOpenAllNews, + onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) }, ) SpacerH(12.dp) @@ -356,23 +357,36 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, items( items = news.content, key = ArticleConfigUM::id, + contentType = { "article" }, ) { article -> + val articleModifier = if (news.content.indexOf(article) == FOURTH_ITEM_INDEX) { + Modifier.onFirstVisible( + minFractionVisible = 0.5f, + callback = feedListCallbacks.onSliderScroll, + ) + } else { + Modifier + } ArticleCard( articleConfigUM = article, onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, - modifier = Modifier + modifier = articleModifier .heightIn(min = 164.dp) .width(216.dp), colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) } - item { + item(contentType = "show_more") { ShowMoreArticlesCard( modifier = Modifier .width(216.dp) - .heightIn(min = 164.dp), - onClick = feedListCallbacks.onOpenAllNews, + .heightIn(min = 164.dp) + .onFirstVisible( + minFractionVisible = 0.5f, + callback = feedListCallbacks.onSliderEndReached, + ), + onClick = { feedListCallbacks.onOpenAllNews(true) }, ) } } @@ -520,6 +534,7 @@ private fun NewsErrorBlock(onRetryClick: () -> Unit) { } 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) 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 565f19a7bf..62d38e2004 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 @@ -32,6 +32,8 @@ internal object FeedListPreviewDataProvider { onOpenAllNews = {}, onMarketItemClick = {}, onSortTypeClick = {}, + onSliderScroll = {}, + onSliderEndReached = {}, ), news = NewsUM( content = articles.filter { it.isTrending.not() }.toImmutableList(), 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 2602fb2395..3d51a5cb48 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 @@ -23,9 +23,11 @@ internal data class FeedListCallbacks( val onSearchClick: () -> Unit, val onMarketOpenClick: (sortBy: SortByTypeUM) -> Unit, val onArticleClick: (id: Int) -> Unit, - val onOpenAllNews: () -> Unit, + val onOpenAllNews: (isFromCarouselButton: Boolean) -> Unit, val onMarketItemClick: (MarketsListItemUM) -> Unit, - val onSortTypeClick: (SortByTypeUM) -> Unit, + val onSortTypeClick: (sortBy: SortByTypeUM) -> Unit, + val onSliderScroll: () -> Unit, + val onSliderEndReached: () -> Unit, ) internal data class FeedListSearchBar(