Updated on 2026-08-14
This commit is contained in:
parent
3ed3dc91b1
commit
6f959bcd3f
21 changed files with 641 additions and 212 deletions
|
|
@ -78,6 +78,7 @@ sealed class AnalyticsParam {
|
|||
data object Onboarding : ScreensSources("Onboarding")
|
||||
data object LongTap : ScreensSources("Long Tap")
|
||||
data object Markets : ScreensSources("Markets")
|
||||
data object MarketPulse : ScreensSources("Market Pulse")
|
||||
data object TangemPay : ScreensSources("Tangem Pay")
|
||||
data object WalletSettings : ScreensSources("Wallet Settings")
|
||||
data object Upgrade : ScreensSources("Upgrade")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.core.analytics.models
|
||||
|
||||
/**
|
||||
* Marker interface for analytics events that should be sent only once per session.
|
||||
*
|
||||
* Events implementing this interface will be tracked by their [oneTimeEventId] to ensure
|
||||
* they are not sent multiple times during the same application session. Once an event
|
||||
* with a specific [oneTimeEventId] has been sent, subsequent attempts to send an event
|
||||
* with the same ID will be ignored.
|
||||
*
|
||||
* @see Analytics.send
|
||||
*/
|
||||
interface OneTimePerSessionEvent {
|
||||
/**
|
||||
* Unique identifier for the one-time event.
|
||||
*
|
||||
* This ID is used to track whether the event has already been sent in the current session.
|
||||
* Events with the same [oneTimeEventId] will only be sent once, even if they are
|
||||
* different instances of the same event class.
|
||||
*/
|
||||
val oneTimeEventId: String
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.common.extensions.toHexString
|
|||
import com.tangem.core.analytics.api.*
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.analytics.models.OneTimePerSessionEvent
|
||||
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
|
@ -31,6 +32,7 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
|
||||
private val handlers = mutableMapOf<String, AnalyticsHandler>()
|
||||
private val paramsInterceptors = ConcurrentHashMap<String, ParamsInterceptor>()
|
||||
private val oneEventsPerSession = ConcurrentHashMap<String, Boolean>()
|
||||
private val analyticsFilters = mutableSetOf<AnalyticsEventFilter>()
|
||||
private val analyticsMutex = Mutex()
|
||||
|
||||
|
|
@ -85,6 +87,11 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
analyticsScope.launch {
|
||||
if (event is OneTimePerSessionEvent &&
|
||||
oneEventsPerSession.putIfAbsent(event.oneTimeEventId, true) != null
|
||||
) {
|
||||
return@launch
|
||||
}
|
||||
event.params = applyParamsInterceptors(event)
|
||||
val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.data.news
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.domain.models.news.NewsError
|
||||
import com.tangem.domain.news.NewsErrorResolver
|
||||
|
||||
internal class DefaultNewsErrorResolver : NewsErrorResolver {
|
||||
|
||||
override fun resolve(throwable: Throwable?): NewsError {
|
||||
return when (throwable) {
|
||||
is ApiResponseError.HttpException -> {
|
||||
NewsError.HttpError(
|
||||
code = throwable.code.numericCode,
|
||||
message = throwable.message.orEmpty(),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
NewsError.NotHttpError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
package com.tangem.data.news.di
|
||||
|
||||
import com.tangem.data.news.DefaultNewsErrorResolver
|
||||
import com.tangem.data.news.repository.DefaultNewsRepository
|
||||
import com.tangem.datasource.api.news.NewsApi
|
||||
import com.tangem.datasource.local.news.details.NewsDetailsStore
|
||||
import com.tangem.datasource.local.news.trending.TrendingNewsStore
|
||||
import com.tangem.datasource.local.news.viewed.NewsViewedStore
|
||||
import com.tangem.domain.news.NewsErrorResolver
|
||||
import com.tangem.domain.news.repository.NewsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -25,6 +27,7 @@ internal object NewsDataModule {
|
|||
newsDetailsStore: NewsDetailsStore,
|
||||
trendingNewsStore: TrendingNewsStore,
|
||||
newsViewedStore: NewsViewedStore,
|
||||
newsErrorResolver: NewsErrorResolver,
|
||||
): NewsRepository {
|
||||
return DefaultNewsRepository(
|
||||
newsApi = newsApi,
|
||||
|
|
@ -32,6 +35,10 @@ internal object NewsDataModule {
|
|||
newsDetailsStore = newsDetailsStore,
|
||||
trendingNewsStore = trendingNewsStore,
|
||||
newsViewedStore = newsViewedStore,
|
||||
newsErrorResolver = newsErrorResolver,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideNewsErrorResolver(): NewsErrorResolver = DefaultNewsErrorResolver()
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import com.tangem.datasource.local.news.details.NewsDetailsStore
|
|||
import com.tangem.datasource.local.news.trending.TrendingNewsStore
|
||||
import com.tangem.datasource.local.news.viewed.NewsViewedStore
|
||||
import com.tangem.domain.models.news.*
|
||||
import com.tangem.domain.news.NewsErrorResolver
|
||||
import com.tangem.domain.news.model.NewsListBatchFlow
|
||||
import com.tangem.domain.news.model.NewsListBatchingContext
|
||||
import com.tangem.domain.news.model.NewsListConfig
|
||||
|
|
@ -32,6 +33,7 @@ internal class DefaultNewsRepository(
|
|||
private val newsDetailsStore: NewsDetailsStore,
|
||||
private val trendingNewsStore: TrendingNewsStore,
|
||||
private val newsViewedStore: NewsViewedStore,
|
||||
private val newsErrorResolver: NewsErrorResolver,
|
||||
) : NewsRepository {
|
||||
|
||||
override fun getNewsListBatchFlow(context: NewsListBatchingContext, batchSize: Int): NewsListBatchFlow {
|
||||
|
|
@ -176,12 +178,7 @@ internal class DefaultNewsRepository(
|
|||
trendingNewsStore.clear()
|
||||
trendingNewsStore.store(
|
||||
key = TRENDING_NEWS_KEY,
|
||||
value = TrendingNews.Error(
|
||||
NewsError.Unknown(
|
||||
message = apiResponse.cause.message,
|
||||
code = null,
|
||||
),
|
||||
),
|
||||
value = TrendingNews.Error(error = newsErrorResolver.resolve(apiResponse.cause.cause)),
|
||||
)
|
||||
}
|
||||
is ApiResponse.Success<NewsTrendingResponse> -> {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,11 @@
|
|||
package com.tangem.domain.models.news
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
sealed interface NewsError {
|
||||
|
||||
@Serializable
|
||||
sealed class NewsError {
|
||||
abstract val message: String?
|
||||
abstract val code: Int?
|
||||
data class HttpError(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
) : NewsError
|
||||
|
||||
data class ArticleNotFound(
|
||||
override val message: String?,
|
||||
override val code: Int?,
|
||||
) : NewsError()
|
||||
|
||||
data class Unknown(
|
||||
override val message: String?,
|
||||
override val code: Int?,
|
||||
) : NewsError()
|
||||
data object NotHttpError : NewsError
|
||||
}
|
||||
|
|
@ -12,5 +12,5 @@ sealed interface TrendingNews {
|
|||
data class Data(val articles: List<ShortArticle>) : TrendingNews
|
||||
|
||||
@Serializable
|
||||
data class Error(val throwable: NewsError) : TrendingNews
|
||||
data class Error(val error: NewsError) : TrendingNews
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.news
|
||||
|
||||
import com.tangem.domain.models.news.NewsError
|
||||
|
||||
interface NewsErrorResolver {
|
||||
|
||||
fun resolve(throwable: Throwable?): NewsError
|
||||
}
|
||||
|
|
@ -90,6 +90,7 @@ dependencies {
|
|||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
/* Common */
|
||||
implementation(projects.common.ui)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ package com.tangem.features.feed.model.feed
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
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
|
||||
|
|
@ -16,13 +19,16 @@ import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase
|
|||
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent
|
||||
import com.tangem.features.feed.model.feed.state.FeedMarketsBatchFlowManager
|
||||
import com.tangem.features.feed.model.feed.state.TrendingNewsStateFactory
|
||||
import com.tangem.features.feed.model.feed.state.FeedStateController
|
||||
import com.tangem.features.feed.model.feed.state.transformers.UpdateGlobalFeedStateTransformer
|
||||
import com.tangem.features.feed.model.feed.state.transformers.UpdateMarketChartsTransformer
|
||||
import com.tangem.features.feed.model.feed.state.transformers.UpdateTrendingNewsStateTransformer
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentHashMap
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -32,18 +38,19 @@ import kotlinx.coroutines.launch
|
|||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.all
|
||||
import kotlin.collections.map
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeedComponentModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase,
|
||||
private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val stateController: FeedStateController,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<DefaultFeedComponent.FeedParams>()
|
||||
|
|
@ -66,23 +73,12 @@ internal class FeedComponentModel @Inject constructor(
|
|||
)
|
||||
|
||||
internal val state: StateFlow<FeedListUM>
|
||||
field = MutableStateFlow<FeedListUM>(initialState())
|
||||
|
||||
private val trendingNewsStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
TrendingNewsStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
onStateUpdate = { newState -> state.update { newState } },
|
||||
onRetryClicked = {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase.invoke()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
get() = stateController.uiState
|
||||
|
||||
val isVisibleOnScreen = MutableStateFlow(false)
|
||||
|
||||
init {
|
||||
initializeState()
|
||||
updateCallbacks()
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
|
|
@ -96,10 +92,50 @@ internal class FeedComponentModel @Inject constructor(
|
|||
flow3 = marketsBatchFlowManager.errorStatesByOrder,
|
||||
flow4 = manageTrendingNewsUseCase.observeTrendingNews(),
|
||||
) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult ->
|
||||
updateMarketCharts(itemsByOrder, loadingStatesByOrder, errorStatesByOrder)
|
||||
trendingNewsStateFactory.updateTrendingNewsState(result = trendingNewsResult)
|
||||
updateGlobalState()
|
||||
val currentSortType = state.value.marketChartConfig.currentSortByType
|
||||
val globalStateTransformer = UpdateGlobalFeedStateTransformer(
|
||||
loadingStatesByOrder = loadingStatesByOrder,
|
||||
errorStatesByOrder = errorStatesByOrder,
|
||||
trendingNewsResult = trendingNewsResult,
|
||||
onRetryClicked = {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
stateController.update { currentState ->
|
||||
currentState.copy(globalState = GlobalFeedState.Loading)
|
||||
}
|
||||
fetchTrendingNewsUseCase.invoke()
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
}
|
||||
},
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
)
|
||||
|
||||
val currentState = stateController.value
|
||||
val newGlobalState = globalStateTransformer.transform(currentState)
|
||||
val shouldSkipIndividualStates = newGlobalState.globalState !is GlobalFeedState.Content
|
||||
|
||||
stateController.update(globalStateTransformer)
|
||||
|
||||
if (!shouldSkipIndividualStates) {
|
||||
stateController.updateAll(
|
||||
UpdateMarketChartsTransformer(
|
||||
itemsByOrder = itemsByOrder,
|
||||
loadingStatesByOrder = loadingStatesByOrder,
|
||||
errorStatesByOrder = errorStatesByOrder,
|
||||
onReloadAll = { marketsBatchFlowManager.reloadAll() },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
),
|
||||
UpdateTrendingNewsStateTransformer(
|
||||
result = trendingNewsResult,
|
||||
onRetryClicked = {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase.invoke()
|
||||
}
|
||||
},
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val currentSortType = stateController.value.marketChartConfig.currentSortByType
|
||||
val items = itemsByOrder[currentSortType]
|
||||
val isLoading = loadingStatesByOrder[currentSortType] == true
|
||||
if (items != null && items.isNotEmpty() && !isLoading) {
|
||||
|
|
@ -127,6 +163,10 @@ internal class FeedComponentModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun initializeState() {
|
||||
stateController.update { initialState() }
|
||||
}
|
||||
|
||||
private fun initialState(): FeedListUM {
|
||||
return FeedListUM(
|
||||
currentDate = getCurrentDate(),
|
||||
|
|
@ -141,6 +181,12 @@ internal class FeedComponentModel @Inject constructor(
|
|||
onOpenAllNews = {},
|
||||
onMarketItemClick = {},
|
||||
onSortTypeClick = {},
|
||||
onSliderScroll = {
|
||||
analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselScrolled())
|
||||
},
|
||||
onSliderEndReached = {
|
||||
analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselEndReached())
|
||||
},
|
||||
),
|
||||
news = NewsUM(
|
||||
content = persistentListOf(),
|
||||
|
|
@ -165,101 +211,8 @@ internal class FeedComponentModel @Inject constructor(
|
|||
return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate)
|
||||
}
|
||||
|
||||
private fun updateMarketCharts(
|
||||
itemsByOrder: Map<SortByTypeUM, ImmutableList<com.tangem.common.ui.markets.models.MarketsListItemUM>>,
|
||||
loadingStatesByOrder: Map<SortByTypeUM, Boolean>,
|
||||
errorStatesByOrder: Map<SortByTypeUM, Boolean>,
|
||||
) {
|
||||
state.update { currentState ->
|
||||
val newMarketCharts = buildMap {
|
||||
SortByTypeUM.entries.forEach { sortByType ->
|
||||
val items = itemsByOrder[sortByType] ?: persistentListOf()
|
||||
val isLoading = loadingStatesByOrder[sortByType] == true
|
||||
val hasError = errorStatesByOrder[sortByType] == true
|
||||
|
||||
when {
|
||||
hasError -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.LoadingError(
|
||||
onRetryClicked = {
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
isLoading -> {
|
||||
put(sortByType, MarketChartUM.Loading)
|
||||
}
|
||||
items.isEmpty() -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.LoadingError(
|
||||
onRetryClicked = {
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.Content(
|
||||
items = items,
|
||||
sortChartConfig = SortChartConfigUM(
|
||||
sortByType = sortByType,
|
||||
isSelected = sortByType == currentState.marketChartConfig.currentSortByType,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toPersistentHashMap()
|
||||
|
||||
currentState.copy(
|
||||
marketChartConfig = currentState.marketChartConfig.copy(
|
||||
marketCharts = newMarketCharts,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGlobalState() {
|
||||
state.update { currentState ->
|
||||
val newsState = currentState.news
|
||||
val marketCharts = currentState.marketChartConfig.marketCharts
|
||||
|
||||
val isNewsLoading = newsState.newsUMState == NewsUMState.LOADING
|
||||
val areAllChartsLoading = marketCharts.values.all { it is MarketChartUM.Loading }
|
||||
|
||||
val isNewsError = newsState.newsUMState == NewsUMState.ERROR
|
||||
val areAllChartsError = marketCharts.values.all { it is MarketChartUM.LoadingError }
|
||||
|
||||
val newGlobalState = when {
|
||||
isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading
|
||||
isNewsError && areAllChartsError -> GlobalFeedState.Error(
|
||||
onRetryClicked = {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase.invoke()
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
}
|
||||
},
|
||||
)
|
||||
else -> GlobalFeedState.Content
|
||||
}
|
||||
|
||||
val currentGlobalState = currentState.globalState
|
||||
if (currentGlobalState::class != newGlobalState::class) {
|
||||
currentState.copy(globalState = newGlobalState)
|
||||
} else {
|
||||
currentState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSortTypeClick(sortByType: SortByTypeUM) {
|
||||
state.update { currentState ->
|
||||
private fun handleSortTypeClicked(sortByType: SortByTypeUM) {
|
||||
stateController.update { currentState ->
|
||||
val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) ->
|
||||
when (chart) {
|
||||
is MarketChartUM.Content -> {
|
||||
|
|
@ -297,11 +250,20 @@ internal class FeedComponentModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun updateCallbacks() {
|
||||
state.update { feedListUM ->
|
||||
stateController.update { feedListUM ->
|
||||
feedListUM.copy(
|
||||
feedListCallbacks = feedListUM.feedListCallbacks.copy(
|
||||
onSortTypeClick = ::onSortTypeClick,
|
||||
onMarketItemClick = { item ->
|
||||
onSortTypeClick = ::handleSortTypeClicked,
|
||||
onMarketItemClick = ::handleMarketItemClicked,
|
||||
onMarketOpenClick = ::handleMarketOpenClicked,
|
||||
onArticleClick = ::handleArticleClicked,
|
||||
onOpenAllNews = ::handleOpenAllNews,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMarketItemClicked(item: MarketsListItemUM) {
|
||||
val tokenMarket = marketsBatchFlowManager.getTokenMarketById(item.id)
|
||||
if (tokenMarket != null) {
|
||||
params.feedClickIntents.onMarketItemClick(
|
||||
|
|
@ -309,30 +271,62 @@ internal class FeedComponentModel @Inject constructor(
|
|||
appCurrency = currentAppCurrency.value,
|
||||
)
|
||||
}
|
||||
},
|
||||
onMarketOpenClick = { sortBy ->
|
||||
}
|
||||
|
||||
private fun handleMarketOpenClicked(sortBy: SortByTypeUM) {
|
||||
when (sortBy) {
|
||||
SortByTypeUM.Rating -> {
|
||||
analyticsEventHandler.send(
|
||||
FeedAnalyticsEvent.TokenListOpened(
|
||||
AnalyticsParam.ScreensSources.Markets,
|
||||
),
|
||||
)
|
||||
}
|
||||
SortByTypeUM.Trending,
|
||||
SortByTypeUM.ExperiencedBuyers,
|
||||
SortByTypeUM.TopGainers,
|
||||
SortByTypeUM.TopLosers,
|
||||
SortByTypeUM.Staking,
|
||||
SortByTypeUM.YieldSupply,
|
||||
-> {
|
||||
analyticsEventHandler.send(
|
||||
FeedAnalyticsEvent.TokenListOpened(
|
||||
AnalyticsParam.ScreensSources.MarketPulse,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
params.feedClickIntents.onMarketOpenClick(sortBy)
|
||||
},
|
||||
onArticleClick = { articleId ->
|
||||
val trendingArticleId = state.value.trendingArticle?.id
|
||||
}
|
||||
|
||||
private fun handleArticleClicked(articleId: Int) {
|
||||
val currentState = stateController.value
|
||||
val trendingArticleId = currentState.trendingArticle?.id
|
||||
if (articleId == trendingArticleId) {
|
||||
analyticsEventHandler.send(
|
||||
event = FeedAnalyticsEvent.NewsCarouselTrendingClicked(newsId = articleId),
|
||||
)
|
||||
}
|
||||
params.feedClickIntents.onArticleClick(
|
||||
articleId = articleId,
|
||||
preselectedArticlesId = listOfNotNull(trendingArticleId) +
|
||||
when (state.value.news.newsUMState) {
|
||||
NewsUMState.CONTENT -> state.value.news.content.map { it.id }
|
||||
when (currentState.news.newsUMState) {
|
||||
NewsUMState.CONTENT -> currentState.news.content.map { it.id }
|
||||
NewsUMState.LOADING,
|
||||
NewsUMState.ERROR,
|
||||
-> emptyList()
|
||||
},
|
||||
paginationConfig = null,
|
||||
)
|
||||
},
|
||||
onOpenAllNews = {
|
||||
params.feedClickIntents.onOpenAllNews()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleOpenAllNews(isFromCarouselButton: Boolean) {
|
||||
if (isFromCarouselButton) {
|
||||
analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselAllNewsButton())
|
||||
} else {
|
||||
analyticsEventHandler.send(FeedAnalyticsEvent.NewsListOpened())
|
||||
}
|
||||
params.feedClickIntents.onOpenAllNews()
|
||||
}
|
||||
|
||||
private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
package com.tangem.features.feed.model.feed.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.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.OneTimePerSessionEvent
|
||||
|
||||
internal sealed class FeedAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category = "Markets", event = event, params = params) {
|
||||
|
||||
data class TokenListOpened(
|
||||
private val screensSources: AnalyticsParam.ScreensSources,
|
||||
) : FeedAnalyticsEvent(
|
||||
event = "Token List Opened",
|
||||
params = mapOf(
|
||||
SOURCE to screensSources.value,
|
||||
),
|
||||
)
|
||||
|
||||
class NewsListOpened : FeedAnalyticsEvent(
|
||||
event = "News List Opened",
|
||||
params = mapOf(
|
||||
SOURCE to AnalyticsParam.ScreensSources.Markets.value,
|
||||
),
|
||||
)
|
||||
|
||||
data class NewsCarouselTrendingClicked(private val newsId: Int) : FeedAnalyticsEvent(
|
||||
event = "News Carousel Trending Clicked",
|
||||
params = mapOf(
|
||||
"News id" to newsId.toString(),
|
||||
),
|
||||
), OneTimePerSessionEvent {
|
||||
override val oneTimeEventId: String = event
|
||||
}
|
||||
|
||||
class NewsCarouselAllNewsButton : FeedAnalyticsEvent(
|
||||
event = "News Carousel All News button",
|
||||
), OneTimePerSessionEvent {
|
||||
override val oneTimeEventId: String = event
|
||||
}
|
||||
|
||||
class NewsCarouselEndReached : FeedAnalyticsEvent(
|
||||
event = "News Carousel End Reached",
|
||||
), OneTimePerSessionEvent {
|
||||
override val oneTimeEventId: String = event
|
||||
}
|
||||
|
||||
class NewsCarouselScrolled : FeedAnalyticsEvent(
|
||||
event = "News Carousel Scrolled",
|
||||
), OneTimePerSessionEvent {
|
||||
override val oneTimeEventId: String = event
|
||||
}
|
||||
|
||||
data class MarketsLoadError(
|
||||
private val code: Int?,
|
||||
private val message: String,
|
||||
) : FeedAnalyticsEvent(
|
||||
event = "Markets Load Error",
|
||||
params = mapOf(
|
||||
ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(),
|
||||
ERROR_MESSAGE to message,
|
||||
),
|
||||
)
|
||||
|
||||
data class NewsLoadError(
|
||||
private val code: Int?,
|
||||
private val message: String,
|
||||
) : FeedAnalyticsEvent(
|
||||
event = "News Load Error",
|
||||
params = mapOf(
|
||||
ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(),
|
||||
ERROR_MESSAGE to message,
|
||||
),
|
||||
)
|
||||
|
||||
data class AllWidgetsLoadError(
|
||||
private val code: Int?,
|
||||
private val message: String,
|
||||
) : FeedAnalyticsEvent(
|
||||
event = "All Widgets Load Error",
|
||||
params = mapOf(
|
||||
ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(),
|
||||
ERROR_MESSAGE to message,
|
||||
),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val IS_NOT_HTTP_ERROR = "Is not http error"
|
||||
}
|
||||
}
|
||||
|
|
@ -66,15 +66,15 @@ internal class FeedMarketsBatchFlowManager(
|
|||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
val errorStatesByOrder: StateFlow<Map<SortByTypeUM, Boolean>> =
|
||||
val errorStatesByOrder: StateFlow<Map<SortByTypeUM, Throwable?>> =
|
||||
combine(
|
||||
TokenMarketListConfig.Order.entries.mapNotNull { order ->
|
||||
managersByOrder[order]?.hasError?.map { hasError -> order to hasError }
|
||||
managersByOrder[order]?.error?.map { error -> order to error }
|
||||
},
|
||||
) { errorStatesList ->
|
||||
errorStatesList.associate { (order, hasError) ->
|
||||
errorStatesList.associate { (order, error) ->
|
||||
val sortByType = order.toSortByTypeUM()
|
||||
sortByType to hasError
|
||||
sortByType to error
|
||||
}
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
|
|
@ -194,19 +194,19 @@ internal class FeedMarketsBatchFlowManager(
|
|||
initialValue = false,
|
||||
)
|
||||
|
||||
val hasError = batchFlow.state
|
||||
val error = batchFlow.state
|
||||
.map { state ->
|
||||
when (val status = state.status) {
|
||||
is PaginationStatus.InitialLoadingError -> true
|
||||
is PaginationStatus.Paginating -> status.lastResult is BatchFetchResult.Error
|
||||
else -> false
|
||||
is PaginationStatus.InitialLoadingError -> status.throwable
|
||||
is PaginationStatus.Paginating -> (status.lastResult as? BatchFetchResult.Error)?.throwable
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = Eagerly,
|
||||
initialValue = false,
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
val onLastBatchLoadedSuccess = batchFlow.state
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.features.feed.model.feed.state
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.features.feed.model.feed.state.transformers.FeedListUMTransformer
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import kotlinx.collections.immutable.persistentHashMapOf
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class FeedStateController @Inject constructor() {
|
||||
|
||||
private val mutableUiState: MutableStateFlow<FeedListUM> = MutableStateFlow(value = getInitialState())
|
||||
|
||||
val uiState: StateFlow<FeedListUM> get() = mutableUiState.asStateFlow()
|
||||
|
||||
val value: FeedListUM get() = uiState.value
|
||||
|
||||
fun update(function: (FeedListUM) -> FeedListUM) {
|
||||
mutableUiState.update(function = function)
|
||||
}
|
||||
|
||||
fun update(transformer: FeedListUMTransformer) {
|
||||
mutableUiState.update(function = transformer::transform)
|
||||
}
|
||||
|
||||
fun updateAll(vararg transformers: FeedListUMTransformer) {
|
||||
transformers.forEach { mutableUiState.update(function = it::transform) }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
mutableUiState.update { getInitialState() }
|
||||
}
|
||||
|
||||
private fun getInitialState(): FeedListUM {
|
||||
return FeedListUM(
|
||||
currentDate = "",
|
||||
feedListSearchBar = FeedListSearchBar(
|
||||
placeholderText = com.tangem.core.ui.extensions.TextReference.EMPTY,
|
||||
onBarClick = {},
|
||||
),
|
||||
feedListCallbacks = FeedListCallbacks(
|
||||
onSearchClick = {},
|
||||
onMarketOpenClick = {},
|
||||
onArticleClick = {},
|
||||
onOpenAllNews = {},
|
||||
onMarketItemClick = {},
|
||||
onSortTypeClick = {},
|
||||
onSliderScroll = {},
|
||||
onSliderEndReached = {},
|
||||
),
|
||||
news = NewsUM(
|
||||
content = persistentListOf(),
|
||||
onRetryClicked = {},
|
||||
newsUMState = NewsUMState.LOADING,
|
||||
),
|
||||
trendingArticle = null,
|
||||
marketChartConfig = MarketChartConfig(
|
||||
marketCharts = persistentHashMapOf(),
|
||||
currentSortByType = SortByTypeUM.Trending,
|
||||
),
|
||||
globalState = GlobalFeedState.Loading,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.features.feed.model.feed.state.transformers
|
||||
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
|
||||
internal interface FeedListUMTransformer {
|
||||
fun transform(prevState: FeedListUM): FeedListUM
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.tangem.features.feed.model.feed.state.transformers
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.domain.models.news.TrendingNews
|
||||
import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
import com.tangem.features.feed.ui.feed.state.GlobalFeedState
|
||||
import com.tangem.features.feed.ui.feed.state.NewsUMState
|
||||
|
||||
internal class UpdateGlobalFeedStateTransformer(
|
||||
private val loadingStatesByOrder: Map<SortByTypeUM, Boolean>,
|
||||
private val errorStatesByOrder: Map<SortByTypeUM, Throwable?>,
|
||||
private val trendingNewsResult: TrendingNews,
|
||||
private val onRetryClicked: () -> Unit,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : FeedListUMTransformer {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
override fun transform(prevState: FeedListUM): FeedListUM {
|
||||
val previousGlobalState = prevState.globalState
|
||||
|
||||
val isNewsLoading = prevState.news.newsUMState == NewsUMState.LOADING
|
||||
val isNewsError = trendingNewsResult is TrendingNews.Error ||
|
||||
prevState.news.newsUMState == NewsUMState.ERROR
|
||||
|
||||
val hasLoadingInCharts = loadingStatesByOrder.values.any { it }
|
||||
val hasErrorInCharts = errorStatesByOrder.values.any { it != null }
|
||||
val hasAllErrorInCharts = errorStatesByOrder.values.isNotEmpty() && errorStatesByOrder.values.all { it != null }
|
||||
|
||||
val areAllChartsLoading = loadingStatesByOrder.values.isNotEmpty() && loadingStatesByOrder.values.all { it }
|
||||
val areAllChartsError = hasAllErrorInCharts || hasErrorInCharts && hasLoadingInCharts
|
||||
|
||||
val newGlobalState = when {
|
||||
isNewsLoading && areAllChartsError -> GlobalFeedState.Loading
|
||||
isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading
|
||||
isNewsError && areAllChartsLoading -> GlobalFeedState.Loading
|
||||
isNewsError && areAllChartsError -> {
|
||||
if (previousGlobalState !is GlobalFeedState.Error) {
|
||||
sendErrorAnalytics(errorStatesByOrder)
|
||||
}
|
||||
GlobalFeedState.Error(
|
||||
onRetryClicked = onRetryClicked,
|
||||
)
|
||||
}
|
||||
else -> GlobalFeedState.Content
|
||||
}
|
||||
|
||||
return prevState.copy(
|
||||
globalState = newGlobalState,
|
||||
news = prevState.news.copy(
|
||||
newsUMState = when {
|
||||
isNewsError -> NewsUMState.ERROR
|
||||
else -> NewsUMState.CONTENT
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendErrorAnalytics(errorStatesByOrder: Map<SortByTypeUM, Throwable?>) {
|
||||
val firstError: Throwable? = errorStatesByOrder.values.firstNotNullOfOrNull { it }
|
||||
firstError?.let { error ->
|
||||
val (code, message) = when (error) {
|
||||
is ApiResponseError.HttpException -> error.code.numericCode to error.message
|
||||
else -> null to ""
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
FeedAnalyticsEvent.AllWidgetsLoadError(
|
||||
code = code,
|
||||
message = message.orEmpty(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.features.feed.model.feed.state.transformers
|
||||
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent
|
||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
import com.tangem.features.feed.ui.feed.state.MarketChartUM
|
||||
import com.tangem.features.feed.ui.feed.state.SortChartConfigUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentHashMap
|
||||
|
||||
internal class UpdateMarketChartsTransformer(
|
||||
private val itemsByOrder: Map<SortByTypeUM, ImmutableList<MarketsListItemUM>>,
|
||||
private val loadingStatesByOrder: Map<SortByTypeUM, Boolean>,
|
||||
private val errorStatesByOrder: Map<SortByTypeUM, Throwable?>,
|
||||
private val onReloadAll: () -> Unit,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : FeedListUMTransformer {
|
||||
|
||||
override fun transform(prevState: FeedListUM): FeedListUM {
|
||||
val newMarketCharts = buildMap {
|
||||
SortByTypeUM.entries.forEach { sortByType ->
|
||||
val items = itemsByOrder[sortByType] ?: persistentListOf()
|
||||
val isLoading = loadingStatesByOrder[sortByType] == true
|
||||
val hasError = errorStatesByOrder[sortByType] != null
|
||||
|
||||
when {
|
||||
hasError -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.LoadingError(
|
||||
onRetryClicked = onReloadAll,
|
||||
),
|
||||
)
|
||||
}
|
||||
isLoading -> {
|
||||
put(sortByType, MarketChartUM.Loading)
|
||||
}
|
||||
items.isEmpty() -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.LoadingError(
|
||||
onRetryClicked = onReloadAll,
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.Content(
|
||||
items = items,
|
||||
sortChartConfig = SortChartConfigUM(
|
||||
sortByType = sortByType,
|
||||
isSelected = sortByType == prevState.marketChartConfig.currentSortByType,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toPersistentHashMap()
|
||||
|
||||
val wasAnyErrorBefore = prevState.marketChartConfig.marketCharts.values.any {
|
||||
it is MarketChartUM.LoadingError
|
||||
}
|
||||
val isAnyErrorNow = newMarketCharts.values.any { it is MarketChartUM.LoadingError }
|
||||
|
||||
if (!wasAnyErrorBefore && isAnyErrorNow) {
|
||||
sendErrorAnalytics(errorStatesByOrder, itemsByOrder)
|
||||
}
|
||||
|
||||
return prevState.copy(
|
||||
marketChartConfig = prevState.marketChartConfig.copy(
|
||||
marketCharts = newMarketCharts,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendErrorAnalytics(
|
||||
errorStatesByOrder: Map<SortByTypeUM, Throwable?>,
|
||||
itemsByOrder: Map<SortByTypeUM, ImmutableList<MarketsListItemUM>>,
|
||||
) {
|
||||
val firstError = errorStatesByOrder.values.firstNotNullOfOrNull { it }
|
||||
if (firstError == null && !itemsByOrder.values.any { it.isEmpty() }) {
|
||||
return
|
||||
}
|
||||
val (code, message) = if (firstError is ApiResponseError.HttpException) {
|
||||
firstError.code.numericCode to firstError.message.orEmpty()
|
||||
} else {
|
||||
null to ""
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
FeedAnalyticsEvent.MarketsLoadError(
|
||||
code = code,
|
||||
message = message,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.features.feed.model.feed.state
|
||||
package com.tangem.features.feed.model.feed.state.transformers
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.models.news.NewsError
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.domain.models.news.TrendingNews
|
||||
import com.tangem.features.feed.model.converter.ShortArticleToArticleConfigUMConverter
|
||||
import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
import com.tangem.features.feed.ui.feed.state.NewsUM
|
||||
import com.tangem.features.feed.ui.feed.state.NewsUMState
|
||||
|
|
@ -10,21 +13,32 @@ import com.tangem.utils.Provider
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class TrendingNewsStateFactory(
|
||||
private val currentStateProvider: Provider<FeedListUM>,
|
||||
private val onStateUpdate: (FeedListUM) -> Unit,
|
||||
internal class UpdateTrendingNewsStateTransformer(
|
||||
private val result: TrendingNews,
|
||||
private val onRetryClicked: () -> Unit,
|
||||
) {
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : FeedListUMTransformer {
|
||||
|
||||
fun updateTrendingNewsState(result: TrendingNews) {
|
||||
val currentState = currentStateProvider()
|
||||
when (result) {
|
||||
is TrendingNews.Data -> handleDataState(currentState, result.articles)
|
||||
is TrendingNews.Error -> handleErrorState(currentState)
|
||||
override fun transform(prevState: FeedListUM): FeedListUM {
|
||||
return when (result) {
|
||||
is TrendingNews.Data -> handleDataState(prevState, result.articles)
|
||||
is TrendingNews.Error -> {
|
||||
val (code, message) = when (val error = result.error) {
|
||||
is NewsError.HttpError -> error.code to error.message
|
||||
NewsError.NotHttpError -> null to ""
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
FeedAnalyticsEvent.NewsLoadError(
|
||||
code = code,
|
||||
message = message,
|
||||
),
|
||||
)
|
||||
handleErrorState(prevState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDataState(currentState: FeedListUM, articles: List<ShortArticle>) {
|
||||
private fun handleDataState(currentState: FeedListUM, articles: List<ShortArticle>): FeedListUM {
|
||||
val (trendingArticle, commonArticles) = separateTrendingAndCommonArticles(articles)
|
||||
val commonArticlesUM = getShortArticleConfigConverter(isTrending = false)
|
||||
.convert(commonArticles)
|
||||
|
|
@ -40,27 +54,23 @@ internal class TrendingNewsStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
onStateUpdate(
|
||||
currentState.copy(
|
||||
return currentState.copy(
|
||||
trendingArticle = trendingArticle?.let { article ->
|
||||
getShortArticleConfigConverter(isTrending = true)
|
||||
.convert(listOf(article))
|
||||
}?.firstOrNull(),
|
||||
news = updatedNews,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleErrorState(currentState: FeedListUM) {
|
||||
onStateUpdate(
|
||||
currentState.copy(
|
||||
private fun handleErrorState(currentState: FeedListUM): FeedListUM {
|
||||
return currentState.copy(
|
||||
trendingArticle = null,
|
||||
news = NewsUM(
|
||||
content = persistentListOf(),
|
||||
onRetryClicked = onRetryClicked,
|
||||
newsUMState = NewsUMState.ERROR,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ import androidx.compose.ui.draw.drawBehind
|
|||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onFirstVisible
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
|
|
@ -329,7 +330,7 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = feedListCallbacks.onOpenAllNews,
|
||||
onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) },
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
|
||||
|
|
@ -356,23 +357,36 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
|||
items(
|
||||
items = news.content,
|
||||
key = ArticleConfigUM::id,
|
||||
contentType = { "article" },
|
||||
) { article ->
|
||||
val articleModifier = if (news.content.indexOf(article) == FOURTH_ITEM_INDEX) {
|
||||
Modifier.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = feedListCallbacks.onSliderScroll,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
modifier = Modifier
|
||||
modifier = articleModifier
|
||||
.heightIn(min = 164.dp)
|
||||
.width(216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
item(contentType = "show_more") {
|
||||
ShowMoreArticlesCard(
|
||||
modifier = Modifier
|
||||
.width(216.dp)
|
||||
.heightIn(min = 164.dp),
|
||||
onClick = feedListCallbacks.onOpenAllNews,
|
||||
.heightIn(min = 164.dp)
|
||||
.onFirstVisible(
|
||||
minFractionVisible = 0.5f,
|
||||
callback = feedListCallbacks.onSliderEndReached,
|
||||
),
|
||||
onClick = { feedListCallbacks.onOpenAllNews(true) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -520,6 +534,7 @@ private fun NewsErrorBlock(onRetryClick: () -> Unit) {
|
|||
}
|
||||
|
||||
private const val DEFAULT_CHART_SIZE_IN_MARKET = 5
|
||||
private const val FOURTH_ITEM_INDEX = 3
|
||||
private const val GRADIENT_START = 0f
|
||||
private const val GRADIENT_END = 0.5f
|
||||
private val LinearGradientFirstPart = Color(0xFF635EEC)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ internal object FeedListPreviewDataProvider {
|
|||
onOpenAllNews = {},
|
||||
onMarketItemClick = {},
|
||||
onSortTypeClick = {},
|
||||
onSliderScroll = {},
|
||||
onSliderEndReached = {},
|
||||
),
|
||||
news = NewsUM(
|
||||
content = articles.filter { it.isTrending.not() }.toImmutableList(),
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@ internal data class FeedListCallbacks(
|
|||
val onSearchClick: () -> Unit,
|
||||
val onMarketOpenClick: (sortBy: SortByTypeUM) -> Unit,
|
||||
val onArticleClick: (id: Int) -> Unit,
|
||||
val onOpenAllNews: () -> Unit,
|
||||
val onOpenAllNews: (isFromCarouselButton: Boolean) -> Unit,
|
||||
val onMarketItemClick: (MarketsListItemUM) -> Unit,
|
||||
val onSortTypeClick: (SortByTypeUM) -> Unit,
|
||||
val onSortTypeClick: (sortBy: SortByTypeUM) -> Unit,
|
||||
val onSliderScroll: () -> Unit,
|
||||
val onSliderEndReached: () -> Unit,
|
||||
)
|
||||
|
||||
internal data class FeedListSearchBar(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue