diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt index 1acfe71d32..9e6153728b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.feed.model.news.list +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -8,6 +9,7 @@ import com.tangem.domain.news.model.NewsListConfig import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase import com.tangem.features.feed.components.news.list.DefaultNewsListComponent +import com.tangem.features.feed.model.news.list.analytics.NewsListAnalyticsEvent import com.tangem.features.feed.model.news.list.statemanager.NewsListStateManager import com.tangem.features.feed.model.news.list.loader.NewsCategoriesLoader import com.tangem.features.feed.model.news.list.statemanager.NewsListBatchFlowManager @@ -31,6 +33,7 @@ internal class NewsListModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val getNewsCategoriesUseCase: GetNewsCategoriesUseCase, private val getNewsListBatchFlowUseCase: GetNewsListBatchFlowUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { @@ -58,6 +61,10 @@ internal class NewsListModel @Inject constructor( ) } + private val newsListStateManager by lazy(LazyThreadSafetyMode.NONE) { + NewsListStateManager(analyticsEventHandler) + } + private val _state = MutableStateFlow( NewsListUM( selectedCategoryId = DEFAULT_ALL_NEWS_CATEGORIES_ID, @@ -95,12 +102,12 @@ internal class NewsListModel @Inject constructor( modelScope.launch(dispatchers.default) { combine( batchFlowManager.uiItems, - batchFlowManager.isInInitialLoadingErrorState, + batchFlowManager.initialLoadingError, batchFlowManager.paginationStatus, - ) { articles, isError, paginationStatus -> - NewsListStateManager.calculateState( + ) { articles, loadingError, paginationStatus -> + newsListStateManager.calculateState( articles = articles, - isError = isError, + error = loadingError, paginationStatus = paginationStatus, onRetryClick = { loadCategories() @@ -125,6 +132,9 @@ internal class NewsListModel @Inject constructor( } else { categoryId } + if (newCategoryId != DEFAULT_ALL_NEWS_CATEGORIES_ID) { + analyticsEventHandler.send(NewsListAnalyticsEvent.NewsCategoriesClick(newCategoryId)) + } selectedCategoryId.value = newCategoryId _state.update { currentState -> currentState.copy( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/analytics/NewsListAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/analytics/NewsListAnalyticsEvent.kt new file mode 100644 index 0000000000..efb0ae3ea6 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/analytics/NewsListAnalyticsEvent.kt @@ -0,0 +1,35 @@ +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 + +internal sealed class NewsListAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = "Markets", event = event, params = params) { + + data class NewsListLoadError( + private val code: Int?, + private val message: String, + ) : NewsListAnalyticsEvent( + event = "News List Load Error", + params = mapOf( + ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(), + ERROR_MESSAGE to message, + ), + ) + + data class NewsCategoriesClick( + private val categoryId: Int, + ) : NewsListAnalyticsEvent( + event = "News Categories Selected", + params = mapOf( + "Selected Categories" to categoryId.toString(), + ), + ) + + private companion object { + const val IS_NOT_HTTP_ERROR = "Is not http error" + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt index 86687df7a8..fa5cff5b5e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt @@ -69,13 +69,20 @@ internal open class NewsListBatchFlowManager( initialValue = persistentListOf(), ) - val isInInitialLoadingErrorState = batchFlow.state - .map { it.status is PaginationStatus.InitialLoadingError } + val initialLoadingError: StateFlow = batchFlow.state + .map { state -> + val status = state.status + if (status is PaginationStatus.InitialLoadingError) { + status.throwable + } else { + null + } + } .distinctUntilChanged() .stateIn( scope = modelScope, started = SharingStarted.Eagerly, - initialValue = false, + initialValue = null, ) val paginationStatus: StateFlow>> = batchFlow.state diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt index c4e30a7ff1..e16a20e9f2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt @@ -1,22 +1,23 @@ package com.tangem.features.feed.model.news.list.statemanager import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.features.feed.model.news.list.analytics.NewsListAnalyticsEvent import com.tangem.features.feed.ui.news.list.state.NewsListState import com.tangem.pagination.PaginationStatus import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -/** - * A pure function for calculating the state of the news list. - * Extracted from NewsListModel to simplify testing and improve readability. - */ -internal object NewsListStateManager { +internal class NewsListStateManager( + private val analyticsEventHandler: AnalyticsEventHandler, +) { /** * Calculates the state of the news list based on the current data. * * @param articles the current list of articles - * @param isError flag indicating an initial loading error + * @param error flag indicating an initial loading error * @param paginationStatus the current pagination status * @param onRetryClick handler for the retry loading action * @param onLoadMore handler for loading the next page @@ -24,13 +25,16 @@ internal object NewsListStateManager { */ fun calculateState( articles: ImmutableList, - isError: Boolean, + error: Throwable?, paginationStatus: PaginationStatus<*>, onRetryClick: () -> Unit, onLoadMore: () -> Unit, ): Pair> { + if (error != null) { + sendErrorAnalytics(error) + } return when { - isError -> NewsListState.LoadingError(onRetryClick) to persistentListOf() + error != null -> NewsListState.LoadingError(onRetryClick) to persistentListOf() paginationStatus is PaginationStatus.InitialLoading && articles.isEmpty() -> { NewsListState.Loading to persistentListOf() } @@ -38,4 +42,17 @@ internal object NewsListStateManager { else -> NewsListState.Content(loadMore = onLoadMore) to articles } } + + private fun sendErrorAnalytics(throwable: Throwable) { + val (code, message) = when (throwable) { + is ApiResponseError.HttpException -> throwable.code.numericCode to throwable.message + else -> null to "" + } + analyticsEventHandler.send( + NewsListAnalyticsEvent.NewsListLoadError( + code = code, + message = message.orEmpty(), + ), + ) + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index f25acdab2d..4a5825783f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* 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.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -354,12 +355,12 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, horizontalArrangement = Arrangement.spacedBy(12.dp), state = listState, ) { - items( + itemsIndexed( items = news.content, - key = ArticleConfigUM::id, - contentType = { "article" }, - ) { article -> - val articleModifier = if (news.content.indexOf(article) == FOURTH_ITEM_INDEX) { + key = { _, article -> article.id }, + contentType = { _, _ -> "article" }, + ) { index, article -> + val articleModifier = if (index == FOURTH_ITEM_INDEX) { Modifier.onFirstVisible( minFractionVisible = 0.5f, callback = feedListCallbacks.onSliderScroll,