Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-16 16:01:13 +05:00
parent 6dc6e28205
commit 3b2a867959
15 changed files with 213 additions and 47 deletions

View file

@ -1,5 +1,7 @@
package com.tangem.core.analytics.models package com.tangem.core.analytics.models
const val IS_NOT_HTTP_ERROR = "Is not http error"
sealed class AnalyticsParam { sealed class AnalyticsParam {
sealed class CardBalanceState(val value: String) { sealed class CardBalanceState(val value: String) {

View file

@ -2,6 +2,7 @@ package com.tangem.data.markets.analytics
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.IS_NOT_HTTP_ERROR
sealed interface MarketsDataAnalyticsEvent { sealed interface MarketsDataAnalyticsEvent {
@ -82,8 +83,4 @@ sealed interface MarketsDataAnalyticsEvent {
Custom("Custom"), Custom("Custom"),
Unknown("Unknown"), Unknown("Unknown"),
} }
private companion object {
const val IS_NOT_HTTP_ERROR = "Is not http error"
}
} }

View file

@ -1,5 +1,9 @@
package com.tangem.data.news.repository package com.tangem.data.news.repository
import arrow.core.Either
import arrow.core.flatten
import arrow.core.left
import arrow.core.right
import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.fold import com.tangem.datasource.api.common.response.fold
@ -90,7 +94,7 @@ internal class DefaultNewsRepository(
.combine(newsLikedStore.getAll()) { articles, likedFlags -> .combine(newsLikedStore.getAll()) { articles, likedFlags ->
articles.map { article -> articles.map { article ->
article.copy( article.copy(
isLiked = isNewsLiked(article.id), isLiked = likedFlags[article.id] == true,
) )
} }
} }
@ -99,9 +103,10 @@ internal class DefaultNewsRepository(
} }
} }
override suspend fun fetchDetailedArticles(newsIds: Collection<Int>, language: String?) { override suspend fun fetchDetailedArticles(
fetchDetailedArticlesInternal(newsIds = newsIds, language = language) newsIds: Collection<Int>,
} language: String?,
): Either<Map<Int, Throwable>, Unit> = fetchDetailedArticlesInternal(newsIds = newsIds, language = language)
override suspend fun fetchTrendingNews(limit: Int, language: String?) { override suspend fun fetchTrendingNews(limit: Int, language: String?) {
fetchAndStoreTrendingNews(limit = limit, language = language) fetchAndStoreTrendingNews(limit = limit, language = language)
@ -145,10 +150,9 @@ internal class DefaultNewsRepository(
return newsLikedStore.getSync()[articleId] == true return newsLikedStore.getSync()[articleId] == true
} }
override suspend fun toggleNewsLiked(articleId: Int): Boolean = withContext(dispatchers.io) { override suspend fun toggleNewsLiked(articleId: Int) = withContext(dispatchers.io) {
val isNewsLikedValue = !isNewsLiked(articleId) val isNewsLikedValue = !isNewsLiked(articleId)
newsLikedStore.updateLiked(listOf(articleId), isNewsLikedValue) newsLikedStore.updateLiked(listOf(articleId), isNewsLikedValue)
isNewsLikedValue
} }
private fun updateViewedStatusForNewsBatch( private fun updateViewedStatusForNewsBatch(
@ -162,9 +166,12 @@ internal class DefaultNewsRepository(
) )
} }
private suspend fun fetchDetailedArticlesInternal(newsIds: Collection<Int>, language: String?) = private suspend fun fetchDetailedArticlesInternal(
newsIds: Collection<Int>,
language: String?,
): Either<Map<Int, Throwable>, Unit> = Either.catch {
withContext(dispatchers.io) { withContext(dispatchers.io) {
if (newsIds.isEmpty()) return@withContext if (newsIds.isEmpty()) return@withContext Unit.right()
val uniqueIds = newsIds.distinct() val uniqueIds = newsIds.distinct()
val idsToFetch = buildList { val idsToFetch = buildList {
@ -174,26 +181,42 @@ internal class DefaultNewsRepository(
} }
} }
if (idsToFetch.isEmpty()) return@withContext if (idsToFetch.isEmpty()) return@withContext Unit.right()
val fetchedArticles = supervisorScope { val fetchedArticles = supervisorScope {
idsToFetch.map { newsId -> idsToFetch.map { newsId ->
async { async {
newsApi.getNewsDetails(newsId = newsId, language = language) Either.catch {
.getOrThrow() newsApi.getNewsDetails(newsId = newsId, language = language)
.toDomainDetailedArticle( .getOrThrow()
isLiked = isNewsLiked(newsId), .toDomainDetailedArticle(
) isLiked = isNewsLiked(newsId),
)
}.mapLeft {
newsId to it
}
} }
}.awaitAll() }.awaitAll()
} }
if (fetchedArticles.isNotEmpty()) { fetchedArticles
newsDetailsStore.store( .filterIsInstance<Either.Right<DetailedArticle>>()
articles = fetchedArticles.associateBy(DetailedArticle::id), .map { it.value }
) .let { articles ->
} if (articles.isNotEmpty()) {
newsDetailsStore.store(
articles = articles.associateBy(DetailedArticle::id),
)
}
}
val errors = fetchedArticles
.filterIsInstance<Either.Left<Pair<Int, Throwable>>>()
.associate { it.value.first to it.value.second }
if (errors.isEmpty()) Unit.right() else errors.left()
} }
}.mapLeft { t -> mapOf(GLOBAL_ERROR_ID to t) }.flatten()
private suspend fun fetchAndStoreTrendingNews(limit: Int, language: String?) { private suspend fun fetchAndStoreTrendingNews(limit: Int, language: String?) {
return withContext(dispatchers.io) { return withContext(dispatchers.io) {
@ -378,5 +401,6 @@ internal class DefaultNewsRepository(
private const val INITIAL_BATCH_KEY = 0 private const val INITIAL_BATCH_KEY = 0
private const val FIRST_PAGE = 1 private const val FIRST_PAGE = 1
private const val TRENDING_NEWS_KEY = "trending_news" private const val TRENDING_NEWS_KEY = "trending_news"
private const val GLOBAL_ERROR_ID = -1
} }
} }

View file

@ -1,5 +1,6 @@
package com.tangem.domain.news.repository package com.tangem.domain.news.repository
import arrow.core.Either
import com.tangem.domain.models.news.ArticleCategory import com.tangem.domain.models.news.ArticleCategory
import com.tangem.domain.models.news.DetailedArticle import com.tangem.domain.models.news.DetailedArticle
import com.tangem.domain.models.news.ShortArticle import com.tangem.domain.models.news.ShortArticle
@ -37,7 +38,7 @@ interface NewsRepository {
/** /**
* Fetches and caches detailed articles for provided ids in parallel. * Fetches and caches detailed articles for provided ids in parallel.
*/ */
suspend fun fetchDetailedArticles(newsIds: Collection<Int>, language: String?) suspend fun fetchDetailedArticles(newsIds: Collection<Int>, language: String?): Either<Map<Int, Throwable>, Unit>
/** /**
* Fetch list of trending news by limit and with correct locale and store it in runtime data store. * Fetch list of trending news by limit and with correct locale and store it in runtime data store.
@ -64,7 +65,6 @@ interface NewsRepository {
/** /**
* Toggle news liked state * Toggle news liked state
* @return an actual liked state of the article
*/ */
suspend fun toggleNewsLiked(articleId: Int): Boolean suspend fun toggleNewsLiked(articleId: Int)
} }

View file

@ -24,7 +24,6 @@ class ObserveNewsDetailsUseCase(
/** /**
* Prefetches the given article ids (can be called with current + next ids for pager preloading). * Prefetches the given article ids (can be called with current + next ids for pager preloading).
*/ */
suspend fun prefetch(newsIds: Collection<Int>, language: String?): Either<Throwable, Unit> = Either.catch { suspend fun prefetch(newsIds: Collection<Int>, language: String?): Either<Map<Int, Throwable>, Unit> =
repository.fetchDetailedArticles(newsIds, language) repository.fetchDetailedArticles(newsIds, language)
}
} }

View file

@ -1,5 +1,6 @@
package com.tangem.domain.news.usecase package com.tangem.domain.news.usecase
import arrow.core.Either
import com.tangem.domain.news.repository.NewsRepository import com.tangem.domain.news.repository.NewsRepository
class ToggleArticleLikedUseCase(private val repository: NewsRepository) { class ToggleArticleLikedUseCase(private val repository: NewsRepository) {
@ -7,7 +8,7 @@ class ToggleArticleLikedUseCase(private val repository: NewsRepository) {
/** /**
* Toggle an article liked state. * Toggle an article liked state.
*/ */
suspend fun toggleLiked(articleId: Int): Boolean { suspend fun toggleLiked(articleId: Int) = Either.catch {
return repository.toggleNewsLiked(articleId) repository.toggleNewsLiked(articleId)
} }
} }

View file

@ -0,0 +1,47 @@
package com.tangem.features.feed.components.market.details.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE
import com.tangem.core.analytics.models.IS_NOT_HTTP_ERROR
import com.tangem.core.analytics.models.OneTimePerSessionEvent
internal sealed class MarketTokenAnalyticsEvent(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = "CoinPage", event = event, params = params) {
data class TokenNewsViewed(
private val token: String,
) : MarketTokenAnalyticsEvent(
event = "Token News Viewed",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
),
), OneTimePerSessionEvent {
override val oneTimeEventId: String = event + token
}
data class TokenNewsCarouselScrolled(
private val token: String,
) : MarketTokenAnalyticsEvent(
event = "Token News Carousel Scrolled",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
),
), OneTimePerSessionEvent {
override val oneTimeEventId: String = event + token
}
data class TokenNewsLoadError(
private val code: Int?,
private val message: String,
) : MarketTokenAnalyticsEvent(
event = "Token News Load Error",
params = mapOf(
ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(),
ERROR_MESSAGE to message,
),
)
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.IS_NOT_HTTP_ERROR
import com.tangem.core.analytics.models.OneTimePerSessionEvent import com.tangem.core.analytics.models.OneTimePerSessionEvent
internal sealed class FeedAnalyticsEvent( internal sealed class FeedAnalyticsEvent(
@ -89,8 +90,4 @@ internal sealed class FeedAnalyticsEvent(
) )
class TokenSearchedClicked : FeedAnalyticsEvent(event = "Token Searched Clicked") class TokenSearchedClicked : FeedAnalyticsEvent(event = "Token Searched Clicked")
private companion object {
const val IS_NOT_HTTP_ERROR = "Is not http error"
}
} }

View file

@ -22,6 +22,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.format.bigdecimal.price import com.tangem.core.ui.format.bigdecimal.price
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains
@ -36,6 +37,7 @@ import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.components.market.details.analytics.MarketTokenAnalyticsEvent
import com.tangem.features.feed.impl.R import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
import com.tangem.features.feed.model.market.details.converter.DescriptionConverter import com.tangem.features.feed.model.market.details.converter.DescriptionConverter
@ -242,6 +244,8 @@ internal class MarketsTokenDetailsModel @Inject constructor(
relatedNews = MarketsTokenDetailsUM.RelatedNews( relatedNews = MarketsTokenDetailsUM.RelatedNews(
articles = persistentListOf(), articles = persistentListOf(),
onArticledClicked = {}, onArticledClicked = {},
onFirstVisible = {},
onScroll = {},
), ),
), ),
) )
@ -322,10 +326,36 @@ internal class MarketsTokenDetailsModel @Inject constructor(
/* preselectedIds */ relatedNews.map { it.id }, /* preselectedIds */ relatedNews.map { it.id },
) )
}, },
onFirstVisible = {
analyticsEventHandler.send(
MarketTokenAnalyticsEvent.TokenNewsViewed(
token = params.token.symbol,
),
)
},
onScroll = {
analyticsEventHandler.send(
MarketTokenAnalyticsEvent.TokenNewsCarouselScrolled(
token = params.token.symbol,
),
)
},
), ),
) )
} }
} }
}.onLeft { throwable ->
val (code, message) = if (throwable is ApiResponseError.HttpException) {
throwable.code.numericCode to throwable.message.orEmpty()
} else {
null to ""
}
analyticsEventHandler.send(
MarketTokenAnalyticsEvent.TokenNewsLoadError(
code = code,
message = message,
),
)
} }
} }
} }

View file

@ -3,11 +3,13 @@ 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.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
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
import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase 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
@ -33,6 +35,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@ -129,7 +132,9 @@ internal class NewsDetailsModel @Inject constructor(
private fun onLikeClick(articleId: Int) { private fun onLikeClick(articleId: Int) {
modelScope.launch { modelScope.launch {
toggleArticleLikedUseCase.toggleLiked(articleId) toggleArticleLikedUseCase
.toggleLiked(articleId)
.onLeft { Timber.e(it) }
analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsLikeClicked(articleId)) analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsLikeClicked(articleId))
} }
} }
@ -202,7 +207,35 @@ internal class NewsDetailsModel @Inject constructor(
observeNewsDetailsUseCase.prefetch( observeNewsDetailsUseCase.prefetch(
newsIds = params.preselectedArticlesId, newsIds = params.preselectedArticlesId,
language = currentLanguage, language = currentLanguage,
).mapLeft { ).onLeft { errors ->
errors.onEach { (newsId, throwable) ->
when {
// an article is opened from deeplink and is not found
params.screenSource == AnalyticsParam.ScreensSources.NewsLink.value &&
newsId == params.articleId &&
throwable is ApiResponseError.HttpException &&
throwable.code == ApiResponseError.HttpException.Code.NOT_FOUND
-> {
analyticsEventHandler.send(
NewsDetailsAnalyticsEvent.NewsLinkMismatch(
newsId = params.articleId,
),
)
}
// global request executing error
newsId < 0 -> {
Timber.e(throwable)
}
else -> {
analyticsEventHandler.send(
NewsDetailsAnalyticsEvent.NewsArticleLoadError(
newsId = newsId,
),
)
}
}
}
}.mapLeft {
stateFactory.createErrorState() stateFactory.createErrorState()
} }
} }

View file

@ -41,4 +41,22 @@ internal sealed class NewsDetailsAnalyticsEvent(
), OneTimePerSessionEvent { ), OneTimePerSessionEvent {
override val oneTimeEventId: String = event + newsId override val oneTimeEventId: String = event + newsId
} }
data class NewsArticleLoadError(
private val newsId: Int,
) : NewsDetailsAnalyticsEvent(
event = "News Article Load Error",
params = mapOf(
"News Id" to newsId.toString(),
),
)
data class NewsLinkMismatch(
private val newsId: Int,
) : NewsDetailsAnalyticsEvent(
event = "News Link Mismatch",
params = mapOf(
"News Id" to newsId.toString(),
),
)
} }

View file

@ -3,6 +3,7 @@ package com.tangem.features.feed.model.news.list.analytics
import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE
import com.tangem.core.analytics.models.IS_NOT_HTTP_ERROR
internal sealed class NewsListAnalyticsEvent( internal sealed class NewsListAnalyticsEvent(
event: String, event: String,
@ -28,8 +29,4 @@ internal sealed class NewsListAnalyticsEvent(
"Selected Categories" to categoryId.toString(), "Selected Categories" to categoryId.toString(),
), ),
) )
private companion object {
const val IS_NOT_HTTP_ERROR = "Is not http error"
}
} }

View file

@ -3,7 +3,7 @@ package com.tangem.features.feed.ui.market.detailed.components
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -11,9 +11,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onFirstVisible
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.common.ui.news.ArticleCard import com.tangem.common.ui.news.ArticleCard
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.items.DescriptionItem import com.tangem.core.ui.components.items.DescriptionItem
@ -24,6 +24,8 @@ import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM
import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.RelatedNews import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.RelatedNews
private const val FOURTH_ITEM_INDEX = 3
@Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA] @Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA]
internal fun LazyListScope.tokenMarketDetailsBody( internal fun LazyListScope.tokenMarketDetailsBody(
state: MarketsTokenDetailsUM.Body, state: MarketsTokenDetailsUM.Body,
@ -215,7 +217,11 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 32.dp, top = 20.dp), .padding(bottom = 32.dp, top = 20.dp)
.onFirstVisible(
minFractionVisible = 0.5f,
callback = relatedNews.onFirstVisible,
),
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
Text( Text(
@ -231,14 +237,23 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
state = listState, state = listState,
) { ) {
items( itemsIndexed(
items = relatedNews.articles, items = relatedNews.articles,
key = ArticleConfigUM::id, key = { index, article -> article.id },
) { article -> ) { index, article ->
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
Modifier.onFirstVisible(
minFractionVisible = 0.5f,
callback = relatedNews.onScroll,
)
} else {
Modifier
}
ArticleCard( ArticleCard(
articleConfigUM = article, articleConfigUM = article,
onArticleClick = { relatedNews.onArticledClicked(article.id) }, onArticleClick = { relatedNews.onArticledClicked(article.id) },
modifier = Modifier modifier = articleModifier
.height(164.dp) .height(164.dp)
.width(216.dp), .width(216.dp),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),

View file

@ -46,6 +46,8 @@ internal object MarketsTokenDetailsPreview {
relatedNews = MarketsTokenDetailsUM.RelatedNews( relatedNews = MarketsTokenDetailsUM.RelatedNews(
articles = persistentListOf(), articles = persistentListOf(),
onArticledClicked = {}, onArticledClicked = {},
onFirstVisible = {},
onScroll = {},
), ),
) )
@ -136,6 +138,8 @@ internal object MarketsTokenDetailsPreview {
relatedNews = MarketsTokenDetailsUM.RelatedNews( relatedNews = MarketsTokenDetailsUM.RelatedNews(
articles = persistentListOf(), articles = persistentListOf(),
onArticledClicked = {}, onArticledClicked = {},
onFirstVisible = {},
onScroll = {},
), ),
) )
} }

View file

@ -76,5 +76,7 @@ internal data class MarketsTokenDetailsUM(
data class RelatedNews( data class RelatedNews(
val articles: ImmutableList<ArticleConfigUM>, val articles: ImmutableList<ArticleConfigUM>,
val onArticledClicked: (id: Int) -> Unit, val onArticledClicked: (id: Int) -> Unit,
val onFirstVisible: () -> Unit,
val onScroll: () -> Unit,
) )
} }