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 a49eceb64c..d3e9da60e7 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 @@ -41,6 +41,11 @@ internal object NewsDomainModule { return MarkArticleAsViewedUseCase(repository) } + @Provides + fun provideToggleArticleLikedUseCase(repository: NewsRepository): ToggleArticleLikedUseCase { + return ToggleArticleLikedUseCase(repository) + } + @Provides fun provideGetNewsUseCase(repository: NewsRepository): GetNewsUseCase { return GetNewsUseCase(repository) 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 fae4659e84..cf64bb4d14 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 @@ -77,6 +77,7 @@ sealed class AnalyticsParam { data object Backup : ScreensSources("Backup") data object Onboarding : ScreensSources("Onboarding") data object LongTap : ScreensSources("Long Tap") + data object Market : ScreensSources("Market") data object Markets : ScreensSources("Markets") data object MarketPulse : ScreensSources("Market Pulse") data object TangemPay : ScreensSources("Tangem Pay") @@ -87,6 +88,9 @@ sealed class AnalyticsParam { data object CreateWalletIntro : ScreensSources("Create Wallet Intro") data object AddNewWallet : ScreensSources("Add New Wallet") data object CreateWallet : ScreensSources("Create Wallet") + data object NewsList : ScreensSources("News List") + data object NewsLink : ScreensSources("News Link") + data object NewsPage : ScreensSources("News Page") } sealed class TxSentFrom(val value: String) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NewsStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NewsStoreModule.kt index 33bd6b280a..22b046ee0c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NewsStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NewsStoreModule.kt @@ -3,6 +3,8 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.news.details.DefaultNewsDetailsStore import com.tangem.datasource.local.news.details.NewsDetailsStore +import com.tangem.datasource.local.news.liked.DefaultNewsLikedStore +import com.tangem.datasource.local.news.liked.NewsLikedStore import com.tangem.datasource.local.news.trending.DefaultTrendingNewsStore import com.tangem.datasource.local.news.trending.TrendingNewsStore import com.tangem.datasource.local.news.viewed.DefaultNewsViewedStore @@ -34,4 +36,10 @@ internal object NewsStoreModule { fun provideNewsViewedStore(): NewsViewedStore { return DefaultNewsViewedStore(store = RuntimeSharedStore()) } + + @Provides + @Singleton + fun provideNewsLikedStore(): NewsLikedStore { + return DefaultNewsLikedStore(store = RuntimeSharedStore()) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/news/liked/DefaultNewsLikedStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/news/liked/DefaultNewsLikedStore.kt new file mode 100644 index 0000000000..cd135334e9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/news/liked/DefaultNewsLikedStore.kt @@ -0,0 +1,32 @@ +package com.tangem.datasource.local.news.liked + +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onStart + +private typealias NewsLikedCache = Map + +internal class DefaultNewsLikedStore( + private val store: RuntimeSharedStore, +) : NewsLikedStore { + + override fun getAll(): Flow> { + return store.get().onStart { emit(emptyMap()) } + } + + override suspend fun getSync(): Map { + return store.getSyncOrNull().orEmpty() + } + + override suspend fun updateLiked(articleIds: Collection, liked: Boolean) { + if (articleIds.isEmpty()) return + + store.update(emptyMap()) { current -> + val updated = current.toMutableMap() + articleIds.forEach { id -> + updated[id] = liked + } + updated + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/news/liked/NewsLikedStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/news/liked/NewsLikedStore.kt new file mode 100644 index 0000000000..6decede719 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/news/liked/NewsLikedStore.kt @@ -0,0 +1,24 @@ +package com.tangem.datasource.local.news.liked + +import kotlinx.coroutines.flow.Flow + +/** + * Store for news liked flags (runtime only). + */ +interface NewsLikedStore { + + /** + * Observes all liked flags. + */ + fun getAll(): Flow> + + /** + * Gets liked flags synchronously (returns empty map if no data). + */ + suspend fun getSync(): Map + + /** + * Updates liked flags for provided article ids. + */ + suspend fun updateLiked(articleIds: Collection, liked: Boolean) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 00b71108b4..8c5d8daa49 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -170,6 +170,8 @@ fun PrimaryButtonIconStart( modifier: Modifier = Modifier, showProgress: Boolean = false, enabled: Boolean = true, + size: TangemButtonSize = TangemButtonSize.Default, + shape: Shape = size.toShape(), ) { TangemButton( modifier = modifier, @@ -180,6 +182,8 @@ fun PrimaryButtonIconStart( enabled = enabled, showProgress = showProgress, textStyle = TangemTheme.typography.subtitle1, + size = size, + shape = shape, ) } // endregion PrimaryButton 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 90851234d3..530884bfcf 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 @@ -4,6 +4,7 @@ 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.liked.NewsLikedStore import com.tangem.datasource.local.news.trending.TrendingNewsStore import com.tangem.datasource.local.news.viewed.NewsViewedStore import com.tangem.domain.news.NewsErrorResolver @@ -27,6 +28,7 @@ internal object NewsDataModule { newsDetailsStore: NewsDetailsStore, trendingNewsStore: TrendingNewsStore, newsViewedStore: NewsViewedStore, + newsLikedStore: NewsLikedStore, newsErrorResolver: NewsErrorResolver, ): NewsRepository { return DefaultNewsRepository( @@ -35,6 +37,7 @@ internal object NewsDataModule { newsDetailsStore = newsDetailsStore, trendingNewsStore = trendingNewsStore, newsViewedStore = newsViewedStore, + newsLikedStore = newsLikedStore, newsErrorResolver = newsErrorResolver, ) } 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 8466016a58..9fbf09d3f3 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 @@ -6,9 +6,13 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.news.NewsApi import com.tangem.datasource.api.news.models.response.NewsTrendingResponse import com.tangem.datasource.local.news.details.NewsDetailsStore +import com.tangem.datasource.local.news.liked.NewsLikedStore 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.models.news.ArticleCategory +import com.tangem.domain.models.news.DetailedArticle +import com.tangem.domain.models.news.ShortArticle +import com.tangem.domain.models.news.TrendingNews import com.tangem.domain.news.NewsErrorResolver import com.tangem.domain.news.model.NewsListBatchFlow import com.tangem.domain.news.model.NewsListBatchingContext @@ -27,12 +31,14 @@ import timber.log.Timber * Implementation of [NewsRepository]. [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class DefaultNewsRepository( private val newsApi: NewsApi, private val dispatchers: CoroutineDispatcherProvider, private val newsDetailsStore: NewsDetailsStore, private val trendingNewsStore: TrendingNewsStore, private val newsViewedStore: NewsViewedStore, + private val newsLikedStore: NewsLikedStore, private val newsErrorResolver: NewsErrorResolver, ) : NewsRepository { @@ -71,9 +77,17 @@ internal class DefaultNewsRepository( } override fun observeDetailedArticles(): Flow> { - return newsDetailsStore.getAll().map { articles -> - articles.associateBy(DetailedArticle::id) - } + return newsDetailsStore.getAll() + .combine(newsLikedStore.getAll()) { articles, likedFlags -> + articles.map { article -> + article.copy( + isLiked = isNewsLiked(article.id), + ) + } + } + .map { articles -> + articles.associateBy(DetailedArticle::id) + } } override suspend fun fetchDetailedArticles(newsIds: Collection, language: String?) { @@ -118,6 +132,16 @@ internal class DefaultNewsRepository( newsViewedStore.updateViewed(articleIds, viewed) } + private suspend fun isNewsLiked(articleId: Int): Boolean { + return newsLikedStore.getSync()[articleId] == true + } + + override suspend fun toggleNewsLiked(articleId: Int): Boolean = withContext(dispatchers.io) { + val isNewsLikedValue = !isNewsLiked(articleId) + newsLikedStore.updateLiked(listOf(articleId), isNewsLikedValue) + isNewsLikedValue + } + private fun updateViewedStatusForNewsBatch( newsBatchFlow: NewsListBatchFlow, scope: CoroutineScope, @@ -148,7 +172,9 @@ internal class DefaultNewsRepository( async { newsApi.getNewsDetails(newsId = newsId, language = language) .getOrThrow() - .toDomainDetailedArticle() + .toDomainDetailedArticle( + isLiked = isNewsLiked(newsId), + ) } }.awaitAll() } diff --git a/data/news/src/main/java/com/tangem/data/news/repository/NewsMapper.kt b/data/news/src/main/java/com/tangem/data/news/repository/NewsMapper.kt index 7f63a42814..50a571f289 100644 --- a/data/news/src/main/java/com/tangem/data/news/repository/NewsMapper.kt +++ b/data/news/src/main/java/com/tangem/data/news/repository/NewsMapper.kt @@ -6,7 +6,7 @@ import com.tangem.datasource.api.news.models.response.NewsOriginalArticleDto import com.tangem.datasource.api.news.models.response.NewsRelatedTokenDto import com.tangem.domain.models.news.* -internal fun NewsDetailsResponse.toDomainDetailedArticle(): DetailedArticle { +internal fun NewsDetailsResponse.toDomainDetailedArticle(isLiked: Boolean): DetailedArticle { return DetailedArticle( id = id, createdAt = createdAt, @@ -20,6 +20,7 @@ internal fun NewsDetailsResponse.toDomainDetailedArticle(): DetailedArticle { shortContent = shortContent, content = content, originalArticles = originalArticles.map { it.toDomainOriginalArticle() }, + isLiked = isLiked, ) } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/news/DetailedArticle.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/news/DetailedArticle.kt index ea41bfe17f..94cc02be56 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/news/DetailedArticle.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/news/DetailedArticle.kt @@ -18,6 +18,7 @@ import kotlinx.serialization.Serializable * @param shortContent - short description * @param content - main text of the article * @param originalArticles - original articles, which were base to build detailed article. + * @param isLiked - flag indicating whether the news is liked or not */ @Serializable data class DetailedArticle( @@ -33,4 +34,5 @@ data class DetailedArticle( val shortContent: String, val content: String, val originalArticles: List, + val isLiked: Boolean, ) \ No newline at end of file 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 4522fe3a7c..cf906001f2 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 @@ -61,4 +61,10 @@ interface NewsRepository { * Updates viewed flag for provided news articles (applies to both regular and trending news). */ suspend fun updateNewsViewed(articleIds: Collection, viewed: Boolean) + + /** + * Toggle news liked state + * @return an actual liked state of the article + */ + suspend fun toggleNewsLiked(articleId: Int): Boolean } \ No newline at end of file diff --git a/domain/news/src/main/java/com/tangem/domain/news/usecase/ToggleArticleLikedUseCase.kt b/domain/news/src/main/java/com/tangem/domain/news/usecase/ToggleArticleLikedUseCase.kt new file mode 100644 index 0000000000..90cac29915 --- /dev/null +++ b/domain/news/src/main/java/com/tangem/domain/news/usecase/ToggleArticleLikedUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.news.usecase + +import com.tangem.domain.news.repository.NewsRepository + +class ToggleArticleLikedUseCase(private val repository: NewsRepository) { + + /** + * Toggle an article liked state. + */ + suspend fun toggleLiked(articleId: Int): Boolean { + return repository.toggleNewsLiked(articleId) + } +} \ No newline at end of file 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 444d17fe6b..0be2031e7d 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 @@ -10,6 +10,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.value.Value +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel @@ -51,7 +52,12 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( ) private val clickIntents = object : FeedEntryClickIntents { - override fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency) { + override fun onMarketItemClick( + token: TokenMarketParams, + appCurrency: AppCurrency, + source: String, + newsId: Int?, + ) { innerRouter.push( route = FeedEntryChildFactory.Child.TokenDetails( params = DefaultMarketsTokenDetailsComponent.Params( @@ -60,11 +66,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( shouldShowPortfolio = true, analyticsParams = DefaultMarketsTokenDetailsComponent.AnalyticsParams( blockchain = null, - source = "Market", + newsId = newsId, + source = source, ), onBackClicked = { onChildBack() }, onArticleClick = { articleId, preselectedArticlesId -> - onArticleClick(articleId, preselectedArticlesId) + onArticleClick( + articleId = articleId, + preselectedArticlesId = preselectedArticlesId, + screenSource = AnalyticsParam.ScreensSources.Markets, + ) }, ), ), @@ -76,7 +87,13 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( route = FeedEntryChildFactory.Child.TokenList( params = DefaultMarketsTokenListComponent.Params( onBackClicked = { onChildBack() }, - onTokenClick = { token, currency -> onMarketItemClick(token, currency) }, + onTokenClick = { token, currency -> + onMarketItemClick( + token = token, + appCurrency = currency, + source = AnalyticsParam.ScreensSources.Market.value, + ) + }, preselectedSortType = sortBy ?: SortByTypeUM.Rating, shouldAlwaysShowSearchBar = sortBy == null, ), @@ -87,16 +104,25 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( override fun onArticleClick( articleId: Int, preselectedArticlesId: List, + screenSource: AnalyticsParam.ScreensSources, paginationConfig: NewsListConfig?, ) { innerRouter.push( FeedEntryChildFactory.Child.NewsDetails( params = DefaultNewsDetailsComponent.Params( + screenSource = screenSource.value, articleId = articleId, onBackClicked = { onChildBack() }, preselectedArticlesId = preselectedArticlesId, paginationConfig = paginationConfig, - onTokenClick = { token, currency -> onMarketItemClick(token, currency) }, + onTokenClick = { token, currency -> + onMarketItemClick( + token = token, + appCurrency = currency, + source = AnalyticsParam.ScreensSources.NewsPage.value, + newsId = articleId, + ) + }, ), ), ) @@ -201,6 +227,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( clickIntents.onArticleClick( articleId = articleId, preselectedArticlesId = preselectedArticlesId, + screenSource = AnalyticsParam.ScreensSources.Token, paginationConfig = null, ) }, @@ -209,17 +236,31 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList( DefaultMarketsTokenListComponent.Params( onBackClicked = { router.pop() }, - onTokenClick = { token, currency -> clickIntents.onMarketItemClick(token, currency) }, + onTokenClick = { token, currency -> + clickIntents.onMarketItemClick( + token = token, + appCurrency = currency, + source = AnalyticsParam.ScreensSources.Market.value, + ) + }, preselectedSortType = SortByTypeUM.Rating, shouldAlwaysShowSearchBar = false, ), ) is FeedEntryRoute.NewsDetail -> FeedEntryChildFactory.Child.NewsDetails( DefaultNewsDetailsComponent.Params( + screenSource = AnalyticsParam.ScreensSources.NewsLink.value, articleId = entryRoute.articleId, onBackClicked = { router.pop() }, preselectedArticlesId = entryRoute.preselectedArticlesId, - onTokenClick = { token, currency -> clickIntents.onMarketItemClick(token, currency) }, + onTokenClick = { token, currency -> + clickIntents.onMarketItemClick( + token = token, + appCurrency = currency, + source = AnalyticsParam.ScreensSources.NewsPage.value, + newsId = entryRoute.articleId, + ) + }, ), ) null -> FeedEntryChildFactory.Child.Feed 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 8bf90f863d..0168a3c88a 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 @@ -2,6 +2,7 @@ package com.tangem.features.feed.components import androidx.compose.runtime.Immutable import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.Route import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent @@ -82,6 +83,7 @@ internal class FeedEntryChildFactory @Inject constructor( feedEntryClickIntents.onArticleClick( articleId = currentArticle, preselectedArticlesId = prefetchedArticles, + screenSource = AnalyticsParam.ScreensSources.NewsList, paginationConfig = paginationConfig, ) }, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt index 31352a5aef..2a561c6ba5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt @@ -78,6 +78,7 @@ internal class DefaultMarketsTokenDetailsComponent( ).screenOpened( blockchain = analyticsParams.blockchain, source = analyticsParams.source, + newsId = analyticsParams.newsId, ), ) } @@ -138,5 +139,6 @@ internal class DefaultMarketsTokenDetailsComponent( data class AnalyticsParams( val blockchain: String?, val source: String, + val newsId: Int? = null, ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt index c663891a5e..38bef9331e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt @@ -60,6 +60,7 @@ internal class DefaultNewsDetailsComponent( @Serializable data class Params( + val screenSource: String, val articleId: Int, val onBackClicked: () -> Unit, val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit), 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 c8d8412ae3..d2690e25b3 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 @@ -269,6 +269,7 @@ internal class FeedComponentModel @Inject constructor( params.feedClickIntents.onMarketItemClick( token = tokenMarket.toSerializableParam(), appCurrency = currentAppCurrency.value, + source = AnalyticsParam.ScreensSources.Market.value, ) } } @@ -317,6 +318,7 @@ internal class FeedComponentModel @Inject constructor( -> emptyList() }, paginationConfig = null, + screenSource = AnalyticsParam.ScreensSources.Markets, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt index d2fa25d6e6..d3ade33796 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.features.feed.model.feed +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.news.model.NewsListConfig @@ -9,18 +10,20 @@ import com.tangem.features.feed.model.market.list.state.SortByTypeUM * Callback interface for feed model navigation actions. */ internal interface FeedModelClickIntents { - fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency) + fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency, source: String, newsId: Int? = null) fun onMarketOpenClick(sortBy: SortByTypeUM?) /** * Method to open article of news from different places. * @param articleId - id of article, which should be opened. * @param preselectedArticlesId - ids of articles, which were prefetched before opening news details. + * @param screenSource - source of screen, from which article was opened. * @param paginationConfig - config of current pagination from news list. Must be null, when open from other places. */ fun onArticleClick( articleId: Int, preselectedArticlesId: List = emptyList(), + screenSource: AnalyticsParam.ScreensSources, paginationConfig: NewsListConfig? = null, ) fun onOpenAllNews() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt index dee935f3d9..05b527c4ff 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt @@ -12,12 +12,13 @@ internal class MarketDetailsAnalyticsEvent( data class EventBuilder( val token: TokenMarketParams, ) { - fun screenOpened(blockchain: String?, source: String) = MarketDetailsAnalyticsEvent( + fun screenOpened(blockchain: String?, source: String, newsId: Int? = null) = MarketDetailsAnalyticsEvent( event = "Token Chart Screen Opened", params = buildMap { put("Token", token.symbol) blockchain?.let { put("blockchain", it) } put("Source", source) + newsId?.let { put("News Id", it.toString()) } }, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt index ab257ebb6b..5dba83b983 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.feed.model.news.details import androidx.compose.runtime.Stable 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.decompose.model.ParamsContainer @@ -11,14 +12,17 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetTokenMarketInfoUseCase import com.tangem.domain.markets.GetTokenPriceChartUseCase +import com.tangem.domain.models.news.OriginalArticle import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase import com.tangem.domain.news.usecase.MarkArticleAsViewedUseCase import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase +import com.tangem.domain.news.usecase.ToggleArticleLikedUseCase import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent -import com.tangem.features.feed.model.news.details.factory.NewsDetailsIndexManager +import com.tangem.features.feed.model.news.details.analytics.NewsDetailsAnalyticsEvent import com.tangem.features.feed.model.news.details.converter.NewsDetailsConverter -import com.tangem.features.feed.model.news.details.loader.NewsRelatedTokensLoader +import com.tangem.features.feed.model.news.details.factory.NewsDetailsIndexManager import com.tangem.features.feed.model.news.details.factory.NewsDetailsStateFactory +import com.tangem.features.feed.model.news.details.loader.NewsRelatedTokensLoader import com.tangem.features.feed.ui.news.details.state.ArticleUM import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM @@ -45,13 +49,15 @@ internal class NewsDetailsModel @Inject constructor( private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, private val markArticleAsViewedUseCase: MarkArticleAsViewedUseCase, + private val toggleArticleLikedUseCase: ToggleArticleLikedUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { private val params = paramsContainer.require() private val currentLanguage = Locale.getDefault().language - private val newsDetailsConverter = NewsDetailsConverter(onSourceClick = urlOpener::openUrl) + private val newsDetailsConverter = NewsDetailsConverter(onSourceClick = ::onOriginalArticleClick) private val paginationManager: NewsDetailsPaginationManager? = params.paginationConfig?.let { config -> NewsDetailsPaginationManager( @@ -90,7 +96,7 @@ internal class NewsDetailsModel @Inject constructor( articles = persistentListOf(), selectedArticleIndex = -1, onShareClick = {}, - onLikeClick = { /* [REDACTED_TODO_COMMENT] */ }, + onLikeClick = ::onLikeClick, onBackClick = params.onBackClicked, onArticleIndexChanged = ::onArticleIndexChanged, ), @@ -108,9 +114,36 @@ internal class NewsDetailsModel @Inject constructor( val state: StateFlow = _state.asStateFlow() init { + trackScreenOpened() loadNews() } + private fun trackScreenOpened() { + analyticsEventHandler.send( + event = NewsDetailsAnalyticsEvent.NewsArticleOpened( + source = params.screenSource, + newsId = params.articleId, + ), + ) + } + + private fun onLikeClick(articleId: Int) { + modelScope.launch { + toggleArticleLikedUseCase.toggleLiked(articleId) + analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsLikeClicked(articleId)) + } + } + + private fun onOriginalArticleClick(originalArticle: OriginalArticle) { + analyticsEventHandler.send( + NewsDetailsAnalyticsEvent.RelatedNewsClicked( + newsId = params.articleId, + relatedNewsId = originalArticle.id, + ), + ) + urlOpener.openUrl(originalArticle.url) + } + private fun onArticleIndexChanged(newIndex: Int) { stateFactory.updateSelectedArticleIndex(newIndex) val currentArticle = when (state.value.articlesStateUM) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/analytics/NewsDetailsAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/analytics/NewsDetailsAnalyticsEvent.kt new file mode 100644 index 0000000000..73b92d193e --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/analytics/NewsDetailsAnalyticsEvent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.feed.model.news.details.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.OneTimePerSessionEvent + +internal sealed class NewsDetailsAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = "Markets", event = event, params = params) { + + data class NewsArticleOpened( + private val source: String, + private val newsId: Int, + ) : NewsDetailsAnalyticsEvent( + event = "News Article Opened", + params = mapOf( + AnalyticsParam.SOURCE to source, + "News Id" to newsId.toString(), + ), + ) + + data class RelatedNewsClicked( + private val newsId: Int, + private val relatedNewsId: Int, + ) : NewsDetailsAnalyticsEvent( + event = "Related News Clicked", + params = mapOf( + "News Id" to newsId.toString(), + "Related News Id" to relatedNewsId.toString(), + ), + ) + + data class NewsLikeClicked( + private val newsId: Int, + ) : NewsDetailsAnalyticsEvent( + event = "News Like Clicked", + params = mapOf( + "News Id" to newsId.toString(), + ), + ), OneTimePerSessionEvent { + override val oneTimeEventId: String = event + newsId + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/NewsDetailsConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/NewsDetailsConverter.kt index a877901aa0..95fd1b69b0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/NewsDetailsConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/NewsDetailsConverter.kt @@ -11,6 +11,7 @@ import com.tangem.core.ui.utils.getFormattedDate import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.news.DetailedArticle +import com.tangem.domain.models.news.OriginalArticle import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.news.details.state.ArticleUM import com.tangem.features.feed.ui.news.details.state.Source @@ -23,7 +24,7 @@ import org.joda.time.DateTime @Stable internal class NewsDetailsConverter( - private val onSourceClick: (String) -> Unit, + private val onSourceClick: (OriginalArticle) -> Unit, ) : Converter { override fun convert(value: DetailedArticle): ArticleUM { @@ -38,6 +39,7 @@ internal class NewsDetailsConverter( sources = buildSources(value), newsUrl = value.newsUrl, relatedTokens = value.relatedTokens.toImmutableList(), + isLiked = value.isLiked, ) } @@ -66,7 +68,7 @@ internal class NewsDetailsConverter( source = Source(id = originalArticle.source.id, name = originalArticle.source.name), publishedAt = mapFormattedDate(originalArticle.publishedAt), url = originalArticle.url, - onClick = { onSourceClick(originalArticle.url) }, + onClick = { onSourceClick(originalArticle) }, imageUrl = originalArticle.imageUrl, ) }.toImmutableList() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt index 0d47363ec9..764fcff180 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt @@ -220,7 +220,7 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) { ) { Text( modifier = Modifier.padding(start = 16.dp), - text = "Related news", + text = stringResourceSafe(R.string.news_related_news), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt index 8010cc035f..f3ab2009a2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt @@ -112,10 +112,11 @@ private fun Content(state: NewsDetailsUM, background: Color) { state = pagerState, modifier = Modifier.fillMaxSize(), ) { page -> + val article = state.articles[page] ArticleDetail( - article = state.articles[page], + article = article, modifier = Modifier.fillMaxSize(), - onLikeClick = state.onLikeClick, + onLikeClick = { state.onLikeClick(article.id) }, relatedTokensUM = state.relatedTokensUM, ) } @@ -176,13 +177,23 @@ private fun ArticleDetail( SpacerH(24.dp) - SecondaryButtonIconStart( - modifier = Modifier.padding(horizontal = 16.dp), - iconResId = R.drawable.ic_heart_20, - text = stringResourceSafe(R.string.news_like), - size = TangemButtonSize.RoundedAction, - onClick = onLikeClick, - ) + if (article.isLiked) { + PrimaryButtonIconStart( + modifier = Modifier.padding(horizontal = 16.dp), + iconResId = R.drawable.ic_heart_20, + text = stringResourceSafe(R.string.news_like), + size = TangemButtonSize.RoundedAction, + onClick = { onLikeClick() }, + ) + } else { + SecondaryButtonIconStart( + modifier = Modifier.padding(horizontal = 16.dp), + iconResId = R.drawable.ic_heart_20, + text = stringResourceSafe(R.string.news_like), + size = TangemButtonSize.RoundedAction, + onClick = { onLikeClick() }, + ) + } RelatedTokensBlock( relatedTokensUM = relatedTokensUM, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/MockArticlesFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/MockArticlesFactory.kt index 4720658f0a..7f051d52fc 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/MockArticlesFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/MockArticlesFactory.kt @@ -48,6 +48,7 @@ internal object MockArticlesFactory { ).toPersistentList(), newsUrl = "", relatedTokens = persistentListOf(), + isLiked = true, ), ArticleUM( id = 2, @@ -76,6 +77,7 @@ internal object MockArticlesFactory { ).toPersistentList(), newsUrl = "", relatedTokens = persistentListOf(), + isLiked = false, ), ArticleUM( id = 3, @@ -104,6 +106,7 @@ internal object MockArticlesFactory { ).toPersistentList(), newsUrl = "", relatedTokens = persistentListOf(), + isLiked = false, ), ArticleUM( id = 4, @@ -132,6 +135,7 @@ internal object MockArticlesFactory { ).toPersistentList(), newsUrl = "", relatedTokens = persistentListOf(), + isLiked = false, ), ArticleUM( id = 5, @@ -146,6 +150,7 @@ internal object MockArticlesFactory { sources = persistentListOf(), newsUrl = "", relatedTokens = persistentListOf(), + isLiked = false, ), ArticleUM( id = 6, @@ -161,6 +166,7 @@ internal object MockArticlesFactory { sources = persistentListOf(), newsUrl = "", relatedTokens = persistentListOf(), + isLiked = false, ), ArticleUM( id = 7, @@ -175,6 +181,7 @@ internal object MockArticlesFactory { sources = persistentListOf(), newsUrl = "", relatedTokens = persistentListOf(), + isLiked = false, ), ArticleUM( id = 8, @@ -189,6 +196,7 @@ internal object MockArticlesFactory { sources = persistentListOf(), newsUrl = "", relatedTokens = persistentListOf(), + isLiked = false, ), ArticleUM( id = 9, @@ -204,6 +212,7 @@ internal object MockArticlesFactory { sources = persistentListOf(), newsUrl = "", relatedTokens = persistentListOf(), + isLiked = false, ), ArticleUM( id = 10, @@ -219,6 +228,7 @@ internal object MockArticlesFactory { sources = persistentListOf(), newsUrl = "", relatedTokens = persistentListOf(), + isLiked = false, ), ).toPersistentList() } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/NewsDetailsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/NewsDetailsUM.kt index f8bcdf5f7d..a3402431c2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/NewsDetailsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/state/NewsDetailsUM.kt @@ -12,7 +12,7 @@ internal data class NewsDetailsUM( val articles: ImmutableList, val selectedArticleIndex: Int, val onShareClick: () -> Unit, - val onLikeClick: () -> Unit, + val onLikeClick: (articleId: Int) -> Unit, val onBackClick: () -> Unit, val onArticleIndexChanged: (Int) -> Unit, val relatedTokensUM: RelatedTokensUM = RelatedTokensUM.Loading, @@ -36,6 +36,7 @@ internal data class ArticleUM( val sources: ImmutableList, val newsUrl: String, val relatedTokens: ImmutableList, + val isLiked: Boolean, ) internal data class SourceUM(