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