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)
|
return MarkArticleAsViewedUseCase(repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideToggleArticleLikedUseCase(repository: NewsRepository): ToggleArticleLikedUseCase {
|
||||||
|
return ToggleArticleLikedUseCase(repository)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideGetNewsUseCase(repository: NewsRepository): GetNewsUseCase {
|
fun provideGetNewsUseCase(repository: NewsRepository): GetNewsUseCase {
|
||||||
return GetNewsUseCase(repository)
|
return GetNewsUseCase(repository)
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ sealed class AnalyticsParam {
|
||||||
data object Backup : ScreensSources("Backup")
|
data object Backup : ScreensSources("Backup")
|
||||||
data object Onboarding : ScreensSources("Onboarding")
|
data object Onboarding : ScreensSources("Onboarding")
|
||||||
data object LongTap : ScreensSources("Long Tap")
|
data object LongTap : ScreensSources("Long Tap")
|
||||||
|
data object Market : ScreensSources("Market")
|
||||||
data object Markets : ScreensSources("Markets")
|
data object Markets : ScreensSources("Markets")
|
||||||
data object MarketPulse : ScreensSources("Market Pulse")
|
data object MarketPulse : ScreensSources("Market Pulse")
|
||||||
data object TangemPay : ScreensSources("Tangem Pay")
|
data object TangemPay : ScreensSources("Tangem Pay")
|
||||||
|
|
@ -87,6 +88,9 @@ sealed class AnalyticsParam {
|
||||||
data object CreateWalletIntro : ScreensSources("Create Wallet Intro")
|
data object CreateWalletIntro : ScreensSources("Create Wallet Intro")
|
||||||
data object AddNewWallet : ScreensSources("Add New Wallet")
|
data object AddNewWallet : ScreensSources("Add New Wallet")
|
||||||
data object CreateWallet : ScreensSources("Create 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) {
|
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.datastore.RuntimeSharedStore
|
||||||
import com.tangem.datasource.local.news.details.DefaultNewsDetailsStore
|
import com.tangem.datasource.local.news.details.DefaultNewsDetailsStore
|
||||||
import com.tangem.datasource.local.news.details.NewsDetailsStore
|
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.DefaultTrendingNewsStore
|
||||||
import com.tangem.datasource.local.news.trending.TrendingNewsStore
|
import com.tangem.datasource.local.news.trending.TrendingNewsStore
|
||||||
import com.tangem.datasource.local.news.viewed.DefaultNewsViewedStore
|
import com.tangem.datasource.local.news.viewed.DefaultNewsViewedStore
|
||||||
|
|
@ -34,4 +36,10 @@ internal object NewsStoreModule {
|
||||||
fun provideNewsViewedStore(): NewsViewedStore {
|
fun provideNewsViewedStore(): NewsViewedStore {
|
||||||
return DefaultNewsViewedStore(store = RuntimeSharedStore())
|
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,
|
modifier: Modifier = Modifier,
|
||||||
showProgress: Boolean = false,
|
showProgress: Boolean = false,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
|
size: TangemButtonSize = TangemButtonSize.Default,
|
||||||
|
shape: Shape = size.toShape(),
|
||||||
) {
|
) {
|
||||||
TangemButton(
|
TangemButton(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -180,6 +182,8 @@ fun PrimaryButtonIconStart(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
showProgress = showProgress,
|
showProgress = showProgress,
|
||||||
textStyle = TangemTheme.typography.subtitle1,
|
textStyle = TangemTheme.typography.subtitle1,
|
||||||
|
size = size,
|
||||||
|
shape = shape,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// endregion PrimaryButton
|
// endregion PrimaryButton
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import com.tangem.data.news.DefaultNewsErrorResolver
|
||||||
import com.tangem.data.news.repository.DefaultNewsRepository
|
import com.tangem.data.news.repository.DefaultNewsRepository
|
||||||
import com.tangem.datasource.api.news.NewsApi
|
import com.tangem.datasource.api.news.NewsApi
|
||||||
import com.tangem.datasource.local.news.details.NewsDetailsStore
|
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.trending.TrendingNewsStore
|
||||||
import com.tangem.datasource.local.news.viewed.NewsViewedStore
|
import com.tangem.datasource.local.news.viewed.NewsViewedStore
|
||||||
import com.tangem.domain.news.NewsErrorResolver
|
import com.tangem.domain.news.NewsErrorResolver
|
||||||
|
|
@ -27,6 +28,7 @@ internal object NewsDataModule {
|
||||||
newsDetailsStore: NewsDetailsStore,
|
newsDetailsStore: NewsDetailsStore,
|
||||||
trendingNewsStore: TrendingNewsStore,
|
trendingNewsStore: TrendingNewsStore,
|
||||||
newsViewedStore: NewsViewedStore,
|
newsViewedStore: NewsViewedStore,
|
||||||
|
newsLikedStore: NewsLikedStore,
|
||||||
newsErrorResolver: NewsErrorResolver,
|
newsErrorResolver: NewsErrorResolver,
|
||||||
): NewsRepository {
|
): NewsRepository {
|
||||||
return DefaultNewsRepository(
|
return DefaultNewsRepository(
|
||||||
|
|
@ -35,6 +37,7 @@ internal object NewsDataModule {
|
||||||
newsDetailsStore = newsDetailsStore,
|
newsDetailsStore = newsDetailsStore,
|
||||||
trendingNewsStore = trendingNewsStore,
|
trendingNewsStore = trendingNewsStore,
|
||||||
newsViewedStore = newsViewedStore,
|
newsViewedStore = newsViewedStore,
|
||||||
|
newsLikedStore = newsLikedStore,
|
||||||
newsErrorResolver = newsErrorResolver,
|
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.NewsApi
|
||||||
import com.tangem.datasource.api.news.models.response.NewsTrendingResponse
|
import com.tangem.datasource.api.news.models.response.NewsTrendingResponse
|
||||||
import com.tangem.datasource.local.news.details.NewsDetailsStore
|
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.trending.TrendingNewsStore
|
||||||
import com.tangem.datasource.local.news.viewed.NewsViewedStore
|
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.NewsErrorResolver
|
||||||
import com.tangem.domain.news.model.NewsListBatchFlow
|
import com.tangem.domain.news.model.NewsListBatchFlow
|
||||||
import com.tangem.domain.news.model.NewsListBatchingContext
|
import com.tangem.domain.news.model.NewsListBatchingContext
|
||||||
|
|
@ -27,12 +31,14 @@ import timber.log.Timber
|
||||||
* Implementation of [NewsRepository].
|
* Implementation of [NewsRepository].
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
internal class DefaultNewsRepository(
|
internal class DefaultNewsRepository(
|
||||||
private val newsApi: NewsApi,
|
private val newsApi: NewsApi,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val newsDetailsStore: NewsDetailsStore,
|
private val newsDetailsStore: NewsDetailsStore,
|
||||||
private val trendingNewsStore: TrendingNewsStore,
|
private val trendingNewsStore: TrendingNewsStore,
|
||||||
private val newsViewedStore: NewsViewedStore,
|
private val newsViewedStore: NewsViewedStore,
|
||||||
|
private val newsLikedStore: NewsLikedStore,
|
||||||
private val newsErrorResolver: NewsErrorResolver,
|
private val newsErrorResolver: NewsErrorResolver,
|
||||||
) : NewsRepository {
|
) : NewsRepository {
|
||||||
|
|
||||||
|
|
@ -71,9 +77,17 @@ internal class DefaultNewsRepository(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observeDetailedArticles(): Flow<Map<Int, DetailedArticle>> {
|
override fun observeDetailedArticles(): Flow<Map<Int, DetailedArticle>> {
|
||||||
return newsDetailsStore.getAll().map { articles ->
|
return newsDetailsStore.getAll()
|
||||||
articles.associateBy(DetailedArticle::id)
|
.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<Int>, language: String?) {
|
override suspend fun fetchDetailedArticles(newsIds: Collection<Int>, language: String?) {
|
||||||
|
|
@ -118,6 +132,16 @@ internal class DefaultNewsRepository(
|
||||||
newsViewedStore.updateViewed(articleIds, viewed)
|
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(
|
private fun updateViewedStatusForNewsBatch(
|
||||||
newsBatchFlow: NewsListBatchFlow,
|
newsBatchFlow: NewsListBatchFlow,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
|
|
@ -148,7 +172,9 @@ internal class DefaultNewsRepository(
|
||||||
async {
|
async {
|
||||||
newsApi.getNewsDetails(newsId = newsId, language = language)
|
newsApi.getNewsDetails(newsId = newsId, language = language)
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.toDomainDetailedArticle()
|
.toDomainDetailedArticle(
|
||||||
|
isLiked = isNewsLiked(newsId),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}.awaitAll()
|
}.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.datasource.api.news.models.response.NewsRelatedTokenDto
|
||||||
import com.tangem.domain.models.news.*
|
import com.tangem.domain.models.news.*
|
||||||
|
|
||||||
internal fun NewsDetailsResponse.toDomainDetailedArticle(): DetailedArticle {
|
internal fun NewsDetailsResponse.toDomainDetailedArticle(isLiked: Boolean): DetailedArticle {
|
||||||
return DetailedArticle(
|
return DetailedArticle(
|
||||||
id = id,
|
id = id,
|
||||||
createdAt = createdAt,
|
createdAt = createdAt,
|
||||||
|
|
@ -20,6 +20,7 @@ internal fun NewsDetailsResponse.toDomainDetailedArticle(): DetailedArticle {
|
||||||
shortContent = shortContent,
|
shortContent = shortContent,
|
||||||
content = content,
|
content = content,
|
||||||
originalArticles = originalArticles.map { it.toDomainOriginalArticle() },
|
originalArticles = originalArticles.map { it.toDomainOriginalArticle() },
|
||||||
|
isLiked = isLiked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import kotlinx.serialization.Serializable
|
||||||
* @param shortContent - short description
|
* @param shortContent - short description
|
||||||
* @param content - main text of the article
|
* @param content - main text of the article
|
||||||
* @param originalArticles - original articles, which were base to build detailed article.
|
* @param originalArticles - original articles, which were base to build detailed article.
|
||||||
|
* @param isLiked - flag indicating whether the news is liked or not
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class DetailedArticle(
|
data class DetailedArticle(
|
||||||
|
|
@ -33,4 +34,5 @@ data class DetailedArticle(
|
||||||
val shortContent: String,
|
val shortContent: String,
|
||||||
val content: String,
|
val content: String,
|
||||||
val originalArticles: List<OriginalArticle>,
|
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).
|
* Updates viewed flag for provided news articles (applies to both regular and trending news).
|
||||||
*/
|
*/
|
||||||
suspend fun updateNewsViewed(articleIds: Collection<Int>, viewed: Boolean)
|
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.childStack
|
||||||
import com.arkivanov.decompose.router.stack.pop
|
import com.arkivanov.decompose.router.stack.pop
|
||||||
import com.arkivanov.decompose.value.Value
|
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.AppComponentContext
|
||||||
import com.tangem.core.decompose.context.childByContext
|
import com.tangem.core.decompose.context.childByContext
|
||||||
import com.tangem.core.decompose.model.getOrCreateModel
|
import com.tangem.core.decompose.model.getOrCreateModel
|
||||||
|
|
@ -51,7 +52,12 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
private val clickIntents = object : FeedEntryClickIntents {
|
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(
|
innerRouter.push(
|
||||||
route = FeedEntryChildFactory.Child.TokenDetails(
|
route = FeedEntryChildFactory.Child.TokenDetails(
|
||||||
params = DefaultMarketsTokenDetailsComponent.Params(
|
params = DefaultMarketsTokenDetailsComponent.Params(
|
||||||
|
|
@ -60,11 +66,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
||||||
shouldShowPortfolio = true,
|
shouldShowPortfolio = true,
|
||||||
analyticsParams = DefaultMarketsTokenDetailsComponent.AnalyticsParams(
|
analyticsParams = DefaultMarketsTokenDetailsComponent.AnalyticsParams(
|
||||||
blockchain = null,
|
blockchain = null,
|
||||||
source = "Market",
|
newsId = newsId,
|
||||||
|
source = source,
|
||||||
),
|
),
|
||||||
onBackClicked = { onChildBack() },
|
onBackClicked = { onChildBack() },
|
||||||
onArticleClick = { articleId, preselectedArticlesId ->
|
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(
|
route = FeedEntryChildFactory.Child.TokenList(
|
||||||
params = DefaultMarketsTokenListComponent.Params(
|
params = DefaultMarketsTokenListComponent.Params(
|
||||||
onBackClicked = { onChildBack() },
|
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,
|
preselectedSortType = sortBy ?: SortByTypeUM.Rating,
|
||||||
shouldAlwaysShowSearchBar = sortBy == null,
|
shouldAlwaysShowSearchBar = sortBy == null,
|
||||||
),
|
),
|
||||||
|
|
@ -87,16 +104,25 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
||||||
override fun onArticleClick(
|
override fun onArticleClick(
|
||||||
articleId: Int,
|
articleId: Int,
|
||||||
preselectedArticlesId: List<Int>,
|
preselectedArticlesId: List<Int>,
|
||||||
|
screenSource: AnalyticsParam.ScreensSources,
|
||||||
paginationConfig: NewsListConfig?,
|
paginationConfig: NewsListConfig?,
|
||||||
) {
|
) {
|
||||||
innerRouter.push(
|
innerRouter.push(
|
||||||
FeedEntryChildFactory.Child.NewsDetails(
|
FeedEntryChildFactory.Child.NewsDetails(
|
||||||
params = DefaultNewsDetailsComponent.Params(
|
params = DefaultNewsDetailsComponent.Params(
|
||||||
|
screenSource = screenSource.value,
|
||||||
articleId = articleId,
|
articleId = articleId,
|
||||||
onBackClicked = { onChildBack() },
|
onBackClicked = { onChildBack() },
|
||||||
preselectedArticlesId = preselectedArticlesId,
|
preselectedArticlesId = preselectedArticlesId,
|
||||||
paginationConfig = paginationConfig,
|
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(
|
clickIntents.onArticleClick(
|
||||||
articleId = articleId,
|
articleId = articleId,
|
||||||
preselectedArticlesId = preselectedArticlesId,
|
preselectedArticlesId = preselectedArticlesId,
|
||||||
|
screenSource = AnalyticsParam.ScreensSources.Token,
|
||||||
paginationConfig = null,
|
paginationConfig = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -209,17 +236,31 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
||||||
FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList(
|
FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList(
|
||||||
DefaultMarketsTokenListComponent.Params(
|
DefaultMarketsTokenListComponent.Params(
|
||||||
onBackClicked = { router.pop() },
|
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,
|
preselectedSortType = SortByTypeUM.Rating,
|
||||||
shouldAlwaysShowSearchBar = false,
|
shouldAlwaysShowSearchBar = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
is FeedEntryRoute.NewsDetail -> FeedEntryChildFactory.Child.NewsDetails(
|
is FeedEntryRoute.NewsDetail -> FeedEntryChildFactory.Child.NewsDetails(
|
||||||
DefaultNewsDetailsComponent.Params(
|
DefaultNewsDetailsComponent.Params(
|
||||||
|
screenSource = AnalyticsParam.ScreensSources.NewsLink.value,
|
||||||
articleId = entryRoute.articleId,
|
articleId = entryRoute.articleId,
|
||||||
onBackClicked = { router.pop() },
|
onBackClicked = { router.pop() },
|
||||||
preselectedArticlesId = entryRoute.preselectedArticlesId,
|
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
|
null -> FeedEntryChildFactory.Child.Feed
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.feed.components
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
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.context.AppComponentContext
|
||||||
import com.tangem.core.decompose.navigation.Route
|
import com.tangem.core.decompose.navigation.Route
|
||||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||||
|
|
@ -82,6 +83,7 @@ internal class FeedEntryChildFactory @Inject constructor(
|
||||||
feedEntryClickIntents.onArticleClick(
|
feedEntryClickIntents.onArticleClick(
|
||||||
articleId = currentArticle,
|
articleId = currentArticle,
|
||||||
preselectedArticlesId = prefetchedArticles,
|
preselectedArticlesId = prefetchedArticles,
|
||||||
|
screenSource = AnalyticsParam.ScreensSources.NewsList,
|
||||||
paginationConfig = paginationConfig,
|
paginationConfig = paginationConfig,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ internal class DefaultMarketsTokenDetailsComponent(
|
||||||
).screenOpened(
|
).screenOpened(
|
||||||
blockchain = analyticsParams.blockchain,
|
blockchain = analyticsParams.blockchain,
|
||||||
source = analyticsParams.source,
|
source = analyticsParams.source,
|
||||||
|
newsId = analyticsParams.newsId,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -138,5 +139,6 @@ internal class DefaultMarketsTokenDetailsComponent(
|
||||||
data class AnalyticsParams(
|
data class AnalyticsParams(
|
||||||
val blockchain: String?,
|
val blockchain: String?,
|
||||||
val source: String,
|
val source: String,
|
||||||
|
val newsId: Int? = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -60,6 +60,7 @@ internal class DefaultNewsDetailsComponent(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Params(
|
data class Params(
|
||||||
|
val screenSource: String,
|
||||||
val articleId: Int,
|
val articleId: Int,
|
||||||
val onBackClicked: () -> Unit,
|
val onBackClicked: () -> Unit,
|
||||||
val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit),
|
val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit),
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,7 @@ internal class FeedComponentModel @Inject constructor(
|
||||||
params.feedClickIntents.onMarketItemClick(
|
params.feedClickIntents.onMarketItemClick(
|
||||||
token = tokenMarket.toSerializableParam(),
|
token = tokenMarket.toSerializableParam(),
|
||||||
appCurrency = currentAppCurrency.value,
|
appCurrency = currentAppCurrency.value,
|
||||||
|
source = AnalyticsParam.ScreensSources.Market.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -317,6 +318,7 @@ internal class FeedComponentModel @Inject constructor(
|
||||||
-> emptyList()
|
-> emptyList()
|
||||||
},
|
},
|
||||||
paginationConfig = null,
|
paginationConfig = null,
|
||||||
|
screenSource = AnalyticsParam.ScreensSources.Markets,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.features.feed.model.feed
|
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.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.markets.TokenMarketParams
|
import com.tangem.domain.markets.TokenMarketParams
|
||||||
import com.tangem.domain.news.model.NewsListConfig
|
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.
|
* Callback interface for feed model navigation actions.
|
||||||
*/
|
*/
|
||||||
internal interface FeedModelClickIntents {
|
internal interface FeedModelClickIntents {
|
||||||
fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency)
|
fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency, source: String, newsId: Int? = null)
|
||||||
fun onMarketOpenClick(sortBy: SortByTypeUM?)
|
fun onMarketOpenClick(sortBy: SortByTypeUM?)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to open article of news from different places.
|
* Method to open article of news from different places.
|
||||||
* @param articleId - id of article, which should be opened.
|
* @param articleId - id of article, which should be opened.
|
||||||
* @param preselectedArticlesId - ids of articles, which were prefetched before opening news details.
|
* @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.
|
* @param paginationConfig - config of current pagination from news list. Must be null, when open from other places.
|
||||||
*/
|
*/
|
||||||
fun onArticleClick(
|
fun onArticleClick(
|
||||||
articleId: Int,
|
articleId: Int,
|
||||||
preselectedArticlesId: List<Int> = emptyList(),
|
preselectedArticlesId: List<Int> = emptyList(),
|
||||||
|
screenSource: AnalyticsParam.ScreensSources,
|
||||||
paginationConfig: NewsListConfig? = null,
|
paginationConfig: NewsListConfig? = null,
|
||||||
)
|
)
|
||||||
fun onOpenAllNews()
|
fun onOpenAllNews()
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,13 @@ internal class MarketDetailsAnalyticsEvent(
|
||||||
data class EventBuilder(
|
data class EventBuilder(
|
||||||
val token: TokenMarketParams,
|
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",
|
event = "Token Chart Screen Opened",
|
||||||
params = buildMap {
|
params = buildMap {
|
||||||
put("Token", token.symbol)
|
put("Token", token.symbol)
|
||||||
blockchain?.let { put("blockchain", it) }
|
blockchain?.let { put("blockchain", it) }
|
||||||
put("Source", source)
|
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 androidx.compose.runtime.Stable
|
||||||
import arrow.core.getOrElse
|
import arrow.core.getOrElse
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
import com.tangem.core.decompose.model.ParamsContainer
|
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.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
|
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
|
||||||
import com.tangem.domain.markets.GetTokenPriceChartUseCase
|
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.GetNewsListBatchFlowUseCase
|
||||||
import com.tangem.domain.news.usecase.MarkArticleAsViewedUseCase
|
import com.tangem.domain.news.usecase.MarkArticleAsViewedUseCase
|
||||||
import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase
|
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.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.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.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.ArticleUM
|
||||||
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
|
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
|
||||||
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
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 getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||||
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
||||||
private val markArticleAsViewedUseCase: MarkArticleAsViewedUseCase,
|
private val markArticleAsViewedUseCase: MarkArticleAsViewedUseCase,
|
||||||
|
private val toggleArticleLikedUseCase: ToggleArticleLikedUseCase,
|
||||||
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
paramsContainer: ParamsContainer,
|
paramsContainer: ParamsContainer,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
||||||
private val params = paramsContainer.require<DefaultNewsDetailsComponent.Params>()
|
private val params = paramsContainer.require<DefaultNewsDetailsComponent.Params>()
|
||||||
private val currentLanguage = Locale.getDefault().language
|
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 ->
|
private val paginationManager: NewsDetailsPaginationManager? = params.paginationConfig?.let { config ->
|
||||||
NewsDetailsPaginationManager(
|
NewsDetailsPaginationManager(
|
||||||
|
|
@ -90,7 +96,7 @@ internal class NewsDetailsModel @Inject constructor(
|
||||||
articles = persistentListOf(),
|
articles = persistentListOf(),
|
||||||
selectedArticleIndex = -1,
|
selectedArticleIndex = -1,
|
||||||
onShareClick = {},
|
onShareClick = {},
|
||||||
onLikeClick = { /* [REDACTED_TODO_COMMENT] */ },
|
onLikeClick = ::onLikeClick,
|
||||||
onBackClick = params.onBackClicked,
|
onBackClick = params.onBackClicked,
|
||||||
onArticleIndexChanged = ::onArticleIndexChanged,
|
onArticleIndexChanged = ::onArticleIndexChanged,
|
||||||
),
|
),
|
||||||
|
|
@ -108,9 +114,36 @@ internal class NewsDetailsModel @Inject constructor(
|
||||||
val state: StateFlow<NewsDetailsUM> = _state.asStateFlow()
|
val state: StateFlow<NewsDetailsUM> = _state.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
trackScreenOpened()
|
||||||
loadNews()
|
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) {
|
private fun onArticleIndexChanged(newIndex: Int) {
|
||||||
stateFactory.updateSelectedArticleIndex(newIndex)
|
stateFactory.updateSelectedArticleIndex(newIndex)
|
||||||
val currentArticle = when (state.value.articlesStateUM) {
|
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.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.news.DetailedArticle
|
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.impl.R
|
||||||
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
||||||
import com.tangem.features.feed.ui.news.details.state.Source
|
import com.tangem.features.feed.ui.news.details.state.Source
|
||||||
|
|
@ -23,7 +24,7 @@ import org.joda.time.DateTime
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
internal class NewsDetailsConverter(
|
internal class NewsDetailsConverter(
|
||||||
private val onSourceClick: (String) -> Unit,
|
private val onSourceClick: (OriginalArticle) -> Unit,
|
||||||
) : Converter<DetailedArticle, ArticleUM> {
|
) : Converter<DetailedArticle, ArticleUM> {
|
||||||
|
|
||||||
override fun convert(value: DetailedArticle): ArticleUM {
|
override fun convert(value: DetailedArticle): ArticleUM {
|
||||||
|
|
@ -38,6 +39,7 @@ internal class NewsDetailsConverter(
|
||||||
sources = buildSources(value),
|
sources = buildSources(value),
|
||||||
newsUrl = value.newsUrl,
|
newsUrl = value.newsUrl,
|
||||||
relatedTokens = value.relatedTokens.toImmutableList(),
|
relatedTokens = value.relatedTokens.toImmutableList(),
|
||||||
|
isLiked = value.isLiked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +68,7 @@ internal class NewsDetailsConverter(
|
||||||
source = Source(id = originalArticle.source.id, name = originalArticle.source.name),
|
source = Source(id = originalArticle.source.id, name = originalArticle.source.name),
|
||||||
publishedAt = mapFormattedDate(originalArticle.publishedAt),
|
publishedAt = mapFormattedDate(originalArticle.publishedAt),
|
||||||
url = originalArticle.url,
|
url = originalArticle.url,
|
||||||
onClick = { onSourceClick(originalArticle.url) },
|
onClick = { onSourceClick(originalArticle) },
|
||||||
imageUrl = originalArticle.imageUrl,
|
imageUrl = originalArticle.imageUrl,
|
||||||
)
|
)
|
||||||
}.toImmutableList()
|
}.toImmutableList()
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
text = "Related news",
|
text = stringResourceSafe(R.string.news_related_news),
|
||||||
style = TangemTheme.typography.h3,
|
style = TangemTheme.typography.h3,
|
||||||
color = TangemTheme.colors.text.primary1,
|
color = TangemTheme.colors.text.primary1,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -112,10 +112,11 @@ private fun Content(state: NewsDetailsUM, background: Color) {
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) { page ->
|
) { page ->
|
||||||
|
val article = state.articles[page]
|
||||||
ArticleDetail(
|
ArticleDetail(
|
||||||
article = state.articles[page],
|
article = article,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
onLikeClick = state.onLikeClick,
|
onLikeClick = { state.onLikeClick(article.id) },
|
||||||
relatedTokensUM = state.relatedTokensUM,
|
relatedTokensUM = state.relatedTokensUM,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -176,13 +177,23 @@ private fun ArticleDetail(
|
||||||
|
|
||||||
SpacerH(24.dp)
|
SpacerH(24.dp)
|
||||||
|
|
||||||
SecondaryButtonIconStart(
|
if (article.isLiked) {
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
PrimaryButtonIconStart(
|
||||||
iconResId = R.drawable.ic_heart_20,
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
text = stringResourceSafe(R.string.news_like),
|
iconResId = R.drawable.ic_heart_20,
|
||||||
size = TangemButtonSize.RoundedAction,
|
text = stringResourceSafe(R.string.news_like),
|
||||||
onClick = onLikeClick,
|
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(
|
RelatedTokensBlock(
|
||||||
relatedTokensUM = relatedTokensUM,
|
relatedTokensUM = relatedTokensUM,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ internal object MockArticlesFactory {
|
||||||
).toPersistentList(),
|
).toPersistentList(),
|
||||||
newsUrl = "",
|
newsUrl = "",
|
||||||
relatedTokens = persistentListOf(),
|
relatedTokens = persistentListOf(),
|
||||||
|
isLiked = true,
|
||||||
),
|
),
|
||||||
ArticleUM(
|
ArticleUM(
|
||||||
id = 2,
|
id = 2,
|
||||||
|
|
@ -76,6 +77,7 @@ internal object MockArticlesFactory {
|
||||||
).toPersistentList(),
|
).toPersistentList(),
|
||||||
newsUrl = "",
|
newsUrl = "",
|
||||||
relatedTokens = persistentListOf(),
|
relatedTokens = persistentListOf(),
|
||||||
|
isLiked = false,
|
||||||
),
|
),
|
||||||
ArticleUM(
|
ArticleUM(
|
||||||
id = 3,
|
id = 3,
|
||||||
|
|
@ -104,6 +106,7 @@ internal object MockArticlesFactory {
|
||||||
).toPersistentList(),
|
).toPersistentList(),
|
||||||
newsUrl = "",
|
newsUrl = "",
|
||||||
relatedTokens = persistentListOf(),
|
relatedTokens = persistentListOf(),
|
||||||
|
isLiked = false,
|
||||||
),
|
),
|
||||||
ArticleUM(
|
ArticleUM(
|
||||||
id = 4,
|
id = 4,
|
||||||
|
|
@ -132,6 +135,7 @@ internal object MockArticlesFactory {
|
||||||
).toPersistentList(),
|
).toPersistentList(),
|
||||||
newsUrl = "",
|
newsUrl = "",
|
||||||
relatedTokens = persistentListOf(),
|
relatedTokens = persistentListOf(),
|
||||||
|
isLiked = false,
|
||||||
),
|
),
|
||||||
ArticleUM(
|
ArticleUM(
|
||||||
id = 5,
|
id = 5,
|
||||||
|
|
@ -146,6 +150,7 @@ internal object MockArticlesFactory {
|
||||||
sources = persistentListOf(),
|
sources = persistentListOf(),
|
||||||
newsUrl = "",
|
newsUrl = "",
|
||||||
relatedTokens = persistentListOf(),
|
relatedTokens = persistentListOf(),
|
||||||
|
isLiked = false,
|
||||||
),
|
),
|
||||||
ArticleUM(
|
ArticleUM(
|
||||||
id = 6,
|
id = 6,
|
||||||
|
|
@ -161,6 +166,7 @@ internal object MockArticlesFactory {
|
||||||
sources = persistentListOf(),
|
sources = persistentListOf(),
|
||||||
newsUrl = "",
|
newsUrl = "",
|
||||||
relatedTokens = persistentListOf(),
|
relatedTokens = persistentListOf(),
|
||||||
|
isLiked = false,
|
||||||
),
|
),
|
||||||
ArticleUM(
|
ArticleUM(
|
||||||
id = 7,
|
id = 7,
|
||||||
|
|
@ -175,6 +181,7 @@ internal object MockArticlesFactory {
|
||||||
sources = persistentListOf(),
|
sources = persistentListOf(),
|
||||||
newsUrl = "",
|
newsUrl = "",
|
||||||
relatedTokens = persistentListOf(),
|
relatedTokens = persistentListOf(),
|
||||||
|
isLiked = false,
|
||||||
),
|
),
|
||||||
ArticleUM(
|
ArticleUM(
|
||||||
id = 8,
|
id = 8,
|
||||||
|
|
@ -189,6 +196,7 @@ internal object MockArticlesFactory {
|
||||||
sources = persistentListOf(),
|
sources = persistentListOf(),
|
||||||
newsUrl = "",
|
newsUrl = "",
|
||||||
relatedTokens = persistentListOf(),
|
relatedTokens = persistentListOf(),
|
||||||
|
isLiked = false,
|
||||||
),
|
),
|
||||||
ArticleUM(
|
ArticleUM(
|
||||||
id = 9,
|
id = 9,
|
||||||
|
|
@ -204,6 +212,7 @@ internal object MockArticlesFactory {
|
||||||
sources = persistentListOf(),
|
sources = persistentListOf(),
|
||||||
newsUrl = "",
|
newsUrl = "",
|
||||||
relatedTokens = persistentListOf(),
|
relatedTokens = persistentListOf(),
|
||||||
|
isLiked = false,
|
||||||
),
|
),
|
||||||
ArticleUM(
|
ArticleUM(
|
||||||
id = 10,
|
id = 10,
|
||||||
|
|
@ -219,6 +228,7 @@ internal object MockArticlesFactory {
|
||||||
sources = persistentListOf(),
|
sources = persistentListOf(),
|
||||||
newsUrl = "",
|
newsUrl = "",
|
||||||
relatedTokens = persistentListOf(),
|
relatedTokens = persistentListOf(),
|
||||||
|
isLiked = false,
|
||||||
),
|
),
|
||||||
).toPersistentList()
|
).toPersistentList()
|
||||||
}
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ internal data class NewsDetailsUM(
|
||||||
val articles: ImmutableList<ArticleUM>,
|
val articles: ImmutableList<ArticleUM>,
|
||||||
val selectedArticleIndex: Int,
|
val selectedArticleIndex: Int,
|
||||||
val onShareClick: () -> Unit,
|
val onShareClick: () -> Unit,
|
||||||
val onLikeClick: () -> Unit,
|
val onLikeClick: (articleId: Int) -> Unit,
|
||||||
val onBackClick: () -> Unit,
|
val onBackClick: () -> Unit,
|
||||||
val onArticleIndexChanged: (Int) -> Unit,
|
val onArticleIndexChanged: (Int) -> Unit,
|
||||||
val relatedTokensUM: RelatedTokensUM = RelatedTokensUM.Loading,
|
val relatedTokensUM: RelatedTokensUM = RelatedTokensUM.Loading,
|
||||||
|
|
@ -36,6 +36,7 @@ internal data class ArticleUM(
|
||||||
val sources: ImmutableList<SourceUM>,
|
val sources: ImmutableList<SourceUM>,
|
||||||
val newsUrl: String,
|
val newsUrl: String,
|
||||||
val relatedTokens: ImmutableList<RelatedToken>,
|
val relatedTokens: ImmutableList<RelatedToken>,
|
||||||
|
val isLiked: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal data class SourceUM(
|
internal data class SourceUM(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue