Updated on 2026-08-14
This commit is contained in:
parent
6dc6e28205
commit
3b2a867959
15 changed files with 213 additions and 47 deletions
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue