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

@ -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_MESSAGE
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
internal sealed class FeedAnalyticsEvent(
@ -89,8 +90,4 @@ internal sealed class FeedAnalyticsEvent(
)
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.percent
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.model.AppCurrency
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.wallets.usecase.GetWalletsUseCase
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.model.market.details.analytics.MarketDetailsAnalyticsEvent
import com.tangem.features.feed.model.market.details.converter.DescriptionConverter
@ -242,6 +244,8 @@ internal class MarketsTokenDetailsModel @Inject constructor(
relatedNews = MarketsTokenDetailsUM.RelatedNews(
articles = persistentListOf(),
onArticledClicked = {},
onFirstVisible = {},
onScroll = {},
),
),
)
@ -322,10 +326,36 @@ internal class MarketsTokenDetailsModel @Inject constructor(
/* 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 arrow.core.getOrElse
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.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
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.model.AppCurrency
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
@ -33,6 +35,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
@ -129,7 +132,9 @@ internal class NewsDetailsModel @Inject constructor(
private fun onLikeClick(articleId: Int) {
modelScope.launch {
toggleArticleLikedUseCase.toggleLiked(articleId)
toggleArticleLikedUseCase
.toggleLiked(articleId)
.onLeft { Timber.e(it) }
analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsLikeClicked(articleId))
}
}
@ -202,7 +207,35 @@ internal class NewsDetailsModel @Inject constructor(
observeNewsDetailsUseCase.prefetch(
newsIds = params.preselectedArticlesId,
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()
}
}

View file

@ -41,4 +41,22 @@ internal sealed class NewsDetailsAnalyticsEvent(
), OneTimePerSessionEvent {
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.AnalyticsParam.Key.ERROR_CODE
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE
import com.tangem.core.analytics.models.IS_NOT_HTTP_ERROR
internal sealed class NewsListAnalyticsEvent(
event: String,
@ -28,8 +29,4 @@ internal sealed class NewsListAnalyticsEvent(
"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.lazy.LazyListScope
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.material3.Text
import androidx.compose.runtime.Composable
@ -11,9 +11,9 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onFirstVisible
import androidx.compose.ui.unit.dp
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.block.TangemBlockCardColors
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.RelatedNews
private const val FOURTH_ITEM_INDEX = 3
@Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA]
internal fun LazyListScope.tokenMarketDetailsBody(
state: MarketsTokenDetailsUM.Body,
@ -215,7 +217,11 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
Column(
modifier = Modifier
.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),
) {
Text(
@ -231,14 +237,23 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
horizontalArrangement = Arrangement.spacedBy(12.dp),
state = listState,
) {
items(
itemsIndexed(
items = relatedNews.articles,
key = ArticleConfigUM::id,
) { article ->
key = { index, article -> article.id },
) { index, article ->
val articleModifier = if (index == FOURTH_ITEM_INDEX) {
Modifier.onFirstVisible(
minFractionVisible = 0.5f,
callback = relatedNews.onScroll,
)
} else {
Modifier
}
ArticleCard(
articleConfigUM = article,
onArticleClick = { relatedNews.onArticledClicked(article.id) },
modifier = Modifier
modifier = articleModifier
.height(164.dp)
.width(216.dp),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),

View file

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

View file

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