Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-15 13:22:08 +01:00
parent 866701cdd6
commit fef3d9fa5e
5 changed files with 90 additions and 20 deletions

View file

@ -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(

View file

@ -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<String, String> = 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"
}
}

View file

@ -69,13 +69,20 @@ internal open class NewsListBatchFlowManager(
initialValue = persistentListOf(),
)
val isInInitialLoadingErrorState = batchFlow.state
.map { it.status is PaginationStatus.InitialLoadingError }
val initialLoadingError: StateFlow<Throwable?> = 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<PaginationStatus<List<ShortArticle>>> = batchFlow.state

View file

@ -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<ArticleConfigUM>,
isError: Boolean,
error: Throwable?,
paginationStatus: PaginationStatus<*>,
onRetryClick: () -> Unit,
onLoadMore: () -> Unit,
): Pair<NewsListState, ImmutableList<ArticleConfigUM>> {
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(),
),
)
}
}

View file

@ -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,