Updated on 2026-08-14
This commit is contained in:
parent
2a5fca2b17
commit
417b2f0906
26 changed files with 313 additions and 32 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int, Boolean>
|
||||
|
||||
internal class DefaultNewsLikedStore(
|
||||
private val store: RuntimeSharedStore<NewsLikedCache>,
|
||||
) : NewsLikedStore {
|
||||
|
||||
override fun getAll(): Flow<Map<Int, Boolean>> {
|
||||
return store.get().onStart { emit(emptyMap()) }
|
||||
}
|
||||
|
||||
override suspend fun getSync(): Map<Int, Boolean> {
|
||||
return store.getSyncOrNull().orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun updateLiked(articleIds: Collection<Int>, liked: Boolean) {
|
||||
if (articleIds.isEmpty()) return
|
||||
|
||||
store.update(emptyMap()) { current ->
|
||||
val updated = current.toMutableMap()
|
||||
articleIds.forEach { id ->
|
||||
updated[id] = liked
|
||||
}
|
||||
updated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Map<Int, Boolean>>
|
||||
|
||||
/**
|
||||
* Gets liked flags synchronously (returns empty map if no data).
|
||||
*/
|
||||
suspend fun getSync(): Map<Int, Boolean>
|
||||
|
||||
/**
|
||||
* Updates liked flags for provided article ids.
|
||||
*/
|
||||
suspend fun updateLiked(articleIds: Collection<Int>, liked: Boolean)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,7 +77,15 @@ internal class DefaultNewsRepository(
|
|||
}
|
||||
|
||||
override fun observeDetailedArticles(): Flow<Map<Int, DetailedArticle>> {
|
||||
return newsDetailsStore.getAll().map { articles ->
|
||||
return newsDetailsStore.getAll()
|
||||
.combine(newsLikedStore.getAll()) { articles, likedFlags ->
|
||||
articles.map { article ->
|
||||
article.copy(
|
||||
isLiked = isNewsLiked(article.id),
|
||||
)
|
||||
}
|
||||
}
|
||||
.map { articles ->
|
||||
articles.associateBy(DetailedArticle::id)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<OriginalArticle>,
|
||||
val isLiked: Boolean,
|
||||
)
|
||||
|
|
@ -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<Int>, viewed: Boolean)
|
||||
|
||||
/**
|
||||
* Toggle news liked state
|
||||
* @return an actual liked state of the article
|
||||
*/
|
||||
suspend fun toggleNewsLiked(articleId: Int): Boolean
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int>,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Int> = emptyList(),
|
||||
screenSource: AnalyticsParam.ScreensSources,
|
||||
paginationConfig: NewsListConfig? = null,
|
||||
)
|
||||
fun onOpenAllNews()
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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<DefaultNewsDetailsComponent.Params>()
|
||||
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<NewsDetailsUM> = _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) {
|
||||
|
|
|
|||
|
|
@ -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<String, String> = 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DetailedArticle, ArticleUM> {
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
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,
|
||||
onClick = { onLikeClick() },
|
||||
)
|
||||
}
|
||||
|
||||
RelatedTokensBlock(
|
||||
relatedTokensUM = relatedTokensUM,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ internal data class NewsDetailsUM(
|
|||
val articles: ImmutableList<ArticleUM>,
|
||||
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<SourceUM>,
|
||||
val newsUrl: String,
|
||||
val relatedTokens: ImmutableList<RelatedToken>,
|
||||
val isLiked: Boolean,
|
||||
)
|
||||
|
||||
internal data class SourceUM(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue