Updated on 2026-08-14
This commit is contained in:
parent
75e81283f8
commit
faaa2aad8f
28 changed files with 795 additions and 316 deletions
|
|
@ -45,8 +45,8 @@ internal class DefaultNewsRepository(
|
||||||
return updateViewedStatusForNewsBatch(newsBatchFlow, context.coroutineScope)
|
return updateViewedStatusForNewsBatch(newsBatchFlow, context.coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getNews(config: NewsListConfig, limit: Int): List<ShortArticle> {
|
override fun getNews(config: NewsListConfig, limit: Int): Flow<List<ShortArticle>> {
|
||||||
return withContext(dispatchers.io) {
|
return flow {
|
||||||
val response = newsApi.getNews(
|
val response = newsApi.getNews(
|
||||||
page = FIRST_PAGE,
|
page = FIRST_PAGE,
|
||||||
limit = limit,
|
limit = limit,
|
||||||
|
|
@ -56,25 +56,16 @@ internal class DefaultNewsRepository(
|
||||||
categoryIds = config.categoryIds.takeIf { it.isNotEmpty() },
|
categoryIds = config.categoryIds.takeIf { it.isNotEmpty() },
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
|
|
||||||
val articles = response.items.map { it.toDomainShortArticle() }
|
val shortArticles = response.items.map { it.toDomainShortArticle() }
|
||||||
val viewedFlags = newsViewedStore.getSync()
|
emit(shortArticles)
|
||||||
|
}
|
||||||
articles.map { article ->
|
.flowOn(dispatchers.io)
|
||||||
val isViewed = viewedFlags[article.id] == true
|
.combine(newsViewedStore.getAll()) { articlesToUpdate, viewedFlags ->
|
||||||
article.copy(viewed = isViewed)
|
articlesToUpdate.map { article ->
|
||||||
|
val isViewed = viewedFlags[article.id] == true
|
||||||
|
article.copy(viewed = isViewed)
|
||||||
|
}.sortedBy { it.viewed }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDetailedArticle(newsId: Int, language: String?): DetailedArticle {
|
|
||||||
val cached = newsDetailsStore.getSyncOrNull(newsId)
|
|
||||||
if (cached != null) return cached
|
|
||||||
|
|
||||||
fetchDetailedArticlesInternal(newsIds = listOf(newsId), language = language)
|
|
||||||
|
|
||||||
return requireNotNull(newsDetailsStore.getSyncOrNull(newsId)) {
|
|
||||||
"Unable to load detailed article with id=$newsId"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observeDetailedArticles(): Flow<Map<Int, DetailedArticle>> {
|
override fun observeDetailedArticles(): Flow<Map<Int, DetailedArticle>> {
|
||||||
|
|
@ -98,10 +89,11 @@ internal class DefaultNewsRepository(
|
||||||
) { trendingNews, viewedFlags ->
|
) { trendingNews, viewedFlags ->
|
||||||
when (trendingNews) {
|
when (trendingNews) {
|
||||||
is TrendingNews.Data -> {
|
is TrendingNews.Data -> {
|
||||||
val articlesWithViewedFlags = trendingNews.articles.map { article ->
|
val articlesWithViewedFlags = trendingNews.articles
|
||||||
val isViewed = viewedFlags[article.id] == true
|
.map { article ->
|
||||||
article.copy(viewed = isViewed)
|
article.copy(viewed = viewedFlags[article.id] == true)
|
||||||
}
|
}
|
||||||
|
.sortedBy { it.viewed }
|
||||||
TrendingNews.Data(articlesWithViewedFlags)
|
TrendingNews.Data(articlesWithViewedFlags)
|
||||||
}
|
}
|
||||||
is TrendingNews.Error -> trendingNews
|
is TrendingNews.Error -> trendingNews
|
||||||
|
|
@ -128,32 +120,11 @@ internal class DefaultNewsRepository(
|
||||||
newsBatchFlow: NewsListBatchFlow,
|
newsBatchFlow: NewsListBatchFlow,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
): NewsListBatchFlow {
|
): NewsListBatchFlow {
|
||||||
return object : NewsListBatchFlow {
|
return NewsBatchFlowWithViewedStatus(
|
||||||
override val state: StateFlow<BatchListState<Int, List<ShortArticle>>> =
|
upstream = newsBatchFlow,
|
||||||
combine(
|
newsViewedStore = newsViewedStore,
|
||||||
newsBatchFlow.state,
|
scope = scope,
|
||||||
newsViewedStore.getAll(),
|
)
|
||||||
) { batchListState, viewedFlags ->
|
|
||||||
val updatedBatches = batchListState.data.map { batch ->
|
|
||||||
val updatedArticles = batch.data.map { article ->
|
|
||||||
val isViewed = viewedFlags[article.id] == true
|
|
||||||
article.copy(viewed = isViewed)
|
|
||||||
}
|
|
||||||
Batch(key = batch.key, data = updatedArticles)
|
|
||||||
}
|
|
||||||
BatchListState(
|
|
||||||
data = updatedBatches,
|
|
||||||
status = batchListState.status,
|
|
||||||
)
|
|
||||||
}.stateIn(
|
|
||||||
scope = scope,
|
|
||||||
started = SharingStarted.Eagerly,
|
|
||||||
initialValue = BatchListState(emptyList(), newsBatchFlow.state.value.status),
|
|
||||||
)
|
|
||||||
|
|
||||||
override val updateResults: SharedFlow<Pair<Nothing, BatchUpdateResult<Int, List<ShortArticle>>>> =
|
|
||||||
newsBatchFlow.updateResults
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchDetailedArticlesInternal(newsIds: Collection<Int>, language: String?) =
|
private suspend fun fetchDetailedArticlesInternal(newsIds: Collection<Int>, language: String?) =
|
||||||
|
|
@ -340,6 +311,37 @@ internal class DefaultNewsRepository(
|
||||||
val params: NewsListConfig,
|
val params: NewsListConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private class NewsBatchFlowWithViewedStatus(
|
||||||
|
private val upstream: NewsListBatchFlow,
|
||||||
|
newsViewedStore: NewsViewedStore,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
) : NewsListBatchFlow {
|
||||||
|
|
||||||
|
override val state: StateFlow<BatchListState<Int, List<ShortArticle>>> = combine(
|
||||||
|
upstream.state,
|
||||||
|
newsViewedStore.getAll(),
|
||||||
|
) { batchListState, viewedFlags ->
|
||||||
|
val updatedBatches = batchListState.data.map { batch ->
|
||||||
|
val updatedArticles = batch.data.map { article ->
|
||||||
|
val isViewed = viewedFlags[article.id] == true
|
||||||
|
article.copy(viewed = isViewed)
|
||||||
|
}
|
||||||
|
Batch(key = batch.key, data = updatedArticles)
|
||||||
|
}
|
||||||
|
BatchListState(
|
||||||
|
data = updatedBatches,
|
||||||
|
status = batchListState.status,
|
||||||
|
)
|
||||||
|
}.stateIn(
|
||||||
|
scope = scope,
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = BatchListState(emptyList(), upstream.state.value.status),
|
||||||
|
)
|
||||||
|
|
||||||
|
override val updateResults: SharedFlow<Pair<Nothing, BatchUpdateResult<Int, List<ShortArticle>>>>
|
||||||
|
get() = upstream.updateResults
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private const val INITIAL_BATCH_KEY = 0
|
private const val INITIAL_BATCH_KEY = 0
|
||||||
private const val FIRST_PAGE = 1
|
private const val FIRST_PAGE = 1
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,11 @@ interface NewsRepository {
|
||||||
fun getNewsListBatchFlow(context: NewsListBatchingContext, batchSize: Int): NewsListBatchFlow
|
fun getNewsListBatchFlow(context: NewsListBatchingContext, batchSize: Int): NewsListBatchFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns list of short article by config.
|
* Returns flow of list of short article by config.
|
||||||
*
|
*
|
||||||
* @param config config for getting news list
|
* @param config config for getting news list
|
||||||
*/
|
*/
|
||||||
suspend fun getNews(config: NewsListConfig, limit: Int): List<ShortArticle>
|
fun getNews(config: NewsListConfig, limit: Int): Flow<List<ShortArticle>>
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns detailed article by id with locale configuration.
|
|
||||||
* @param newsId news identification
|
|
||||||
* @param language current locale
|
|
||||||
*/
|
|
||||||
suspend fun getDetailedArticle(newsId: Int, language: String?): DetailedArticle
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observes cached detailed articles.
|
* Observes cached detailed articles.
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ import arrow.core.Either
|
||||||
import com.tangem.domain.models.news.ShortArticle
|
import com.tangem.domain.models.news.ShortArticle
|
||||||
import com.tangem.domain.news.model.NewsListConfig
|
import com.tangem.domain.news.model.NewsListConfig
|
||||||
import com.tangem.domain.news.repository.NewsRepository
|
import com.tangem.domain.news.repository.NewsRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class GetNewsUseCase(private val repository: NewsRepository) {
|
class GetNewsUseCase(private val repository: NewsRepository) {
|
||||||
|
|
||||||
suspend fun getNews(limit: Int, newsListConfig: NewsListConfig): Either<Throwable, List<ShortArticle>> =
|
fun getNews(limit: Int, newsListConfig: NewsListConfig): Either<Throwable, Flow<List<ShortArticle>>> =
|
||||||
Either.catch {
|
Either.catch {
|
||||||
repository.getNews(
|
repository.getNews(
|
||||||
config = newsListConfig,
|
config = newsListConfig,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.markets.TokenMarketParams
|
import com.tangem.domain.markets.TokenMarketParams
|
||||||
|
import com.tangem.domain.news.model.NewsListConfig
|
||||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||||
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
||||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||||
|
|
@ -79,13 +80,18 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onArticleClick(articleId: Int, preselectedArticlesId: List<Int>) {
|
override fun onArticleClick(
|
||||||
|
articleId: Int,
|
||||||
|
preselectedArticlesId: List<Int>,
|
||||||
|
paginationConfig: NewsListConfig?,
|
||||||
|
) {
|
||||||
innerRouter.push(
|
innerRouter.push(
|
||||||
FeedEntryChildFactory.Child.NewsDetails(
|
FeedEntryChildFactory.Child.NewsDetails(
|
||||||
params = DefaultNewsDetailsComponent.Params(
|
params = DefaultNewsDetailsComponent.Params(
|
||||||
articleId = articleId,
|
articleId = articleId,
|
||||||
onBackClicked = { onChildBack() },
|
onBackClicked = { onChildBack() },
|
||||||
preselectedArticlesId = preselectedArticlesId,
|
preselectedArticlesId = preselectedArticlesId,
|
||||||
|
paginationConfig = paginationConfig,
|
||||||
onTokenClick = { token, currency -> onMarketItemClick(token, currency) },
|
onTokenClick = { token, currency -> onMarketItemClick(token, currency) },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -179,7 +185,11 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
||||||
},
|
},
|
||||||
onBackClicked = { router.pop() },
|
onBackClicked = { router.pop() },
|
||||||
onArticleClick = { articleId, preselectedArticlesId ->
|
onArticleClick = { articleId, preselectedArticlesId ->
|
||||||
clickIntents.onArticleClick(articleId, preselectedArticlesId)
|
clickIntents.onArticleClick(
|
||||||
|
articleId = articleId,
|
||||||
|
preselectedArticlesId = preselectedArticlesId,
|
||||||
|
paginationConfig = null,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,11 @@ internal class FeedEntryChildFactory @Inject constructor(
|
||||||
DefaultNewsListComponent(
|
DefaultNewsListComponent(
|
||||||
appComponentContext = appComponentContext,
|
appComponentContext = appComponentContext,
|
||||||
params = DefaultNewsListComponent.Params(
|
params = DefaultNewsListComponent.Params(
|
||||||
onArticleClicked = { currentArticle, prefetchedArticles ->
|
onArticleClicked = { currentArticle, prefetchedArticles, paginationConfig ->
|
||||||
feedEntryClickIntents.onArticleClick(
|
feedEntryClickIntents.onArticleClick(
|
||||||
articleId = currentArticle,
|
articleId = currentArticle,
|
||||||
preselectedArticlesId = prefetchedArticles,
|
preselectedArticlesId = prefetchedArticles,
|
||||||
|
paginationConfig = paginationConfig,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onBackClick = onBackClicked,
|
onBackClick = onBackClicked,
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,10 @@ import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.markets.TokenMarketParams
|
import com.tangem.domain.markets.TokenMarketParams
|
||||||
|
import com.tangem.domain.news.model.NewsListConfig
|
||||||
import com.tangem.features.feed.model.news.details.NewsDetailsModel
|
import com.tangem.features.feed.model.news.details.NewsDetailsModel
|
||||||
import com.tangem.features.feed.ui.news.details.NewsDetailsContent
|
import com.tangem.features.feed.ui.news.details.NewsDetailsContent
|
||||||
|
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
internal class DefaultNewsDetailsComponent(
|
internal class DefaultNewsDetailsComponent(
|
||||||
|
|
@ -41,7 +43,8 @@ internal class DefaultNewsDetailsComponent(
|
||||||
endButton = TopAppBarButtonUM.Icon(
|
endButton = TopAppBarButtonUM.Icon(
|
||||||
iconRes = R.drawable.ic_share_24,
|
iconRes = R.drawable.ic_share_24,
|
||||||
onClicked = state.onShareClick,
|
onClicked = state.onShareClick,
|
||||||
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
|
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED &&
|
||||||
|
state.articlesStateUM is ArticlesStateUM.Content,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -61,5 +64,6 @@ internal class DefaultNewsDetailsComponent(
|
||||||
val onBackClicked: () -> Unit,
|
val onBackClicked: () -> Unit,
|
||||||
val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit),
|
val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit),
|
||||||
val preselectedArticlesId: List<Int> = emptyList(),
|
val preselectedArticlesId: List<Int> = emptyList(),
|
||||||
|
val paginationConfig: NewsListConfig? = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||||
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
|
||||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||||
|
import com.tangem.domain.news.model.NewsListConfig
|
||||||
import com.tangem.features.feed.model.news.list.NewsListModel
|
import com.tangem.features.feed.model.news.list.NewsListModel
|
||||||
import com.tangem.features.feed.ui.news.list.NewsListContent
|
import com.tangem.features.feed.ui.news.list.NewsListContent
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
@ -51,7 +52,11 @@ internal class DefaultNewsListComponent(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Params(
|
data class Params(
|
||||||
val onArticleClicked: (currentArticle: Int, prefetchedArticles: List<Int>) -> Unit,
|
val onArticleClicked: (
|
||||||
|
currentArticle: Int,
|
||||||
|
prefetchedArticles: List<Int>,
|
||||||
|
paginationConfig: NewsListConfig?,
|
||||||
|
) -> Unit,
|
||||||
val onBackClick: () -> Unit,
|
val onBackClick: () -> Unit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,12 @@ internal class BatchListStateManager<Key, Domain, UI>(
|
||||||
private val converter: BatchItemConverter<Domain, UI>,
|
private val converter: BatchItemConverter<Domain, UI>,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) {
|
) {
|
||||||
val state = MutableStateFlow(BatchListState<Key, Domain, UI>())
|
val state = MutableStateFlow(
|
||||||
|
BatchListState<Key, Domain, UI>(
|
||||||
|
uiBatches = emptyList(),
|
||||||
|
processedItems = emptyList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun update(newList: List<Batch<Key, List<Domain>>>, forceUpdate: Boolean) =
|
suspend fun update(newList: List<Batch<Key, List<Domain>>>, forceUpdate: Boolean) =
|
||||||
withContext(dispatchers.default) {
|
withContext(dispatchers.default) {
|
||||||
|
|
@ -26,7 +31,7 @@ internal class BatchListStateManager<Key, Domain, UI>(
|
||||||
}
|
}
|
||||||
|
|
||||||
val isInitialLoading = forceUpdate ||
|
val isInitialLoading = forceUpdate ||
|
||||||
previousList.isNullOrEmpty() ||
|
previousList.isEmpty() ||
|
||||||
newList.firstOrNull()?.key != previousList.firstOrNull()?.key
|
newList.firstOrNull()?.key != previousList.firstOrNull()?.key
|
||||||
|
|
||||||
val outItems = if (isInitialLoading) {
|
val outItems = if (isInitialLoading) {
|
||||||
|
|
@ -77,8 +82,8 @@ internal class BatchListStateManager<Key, Domain, UI>(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class BatchListState<K, Domain, UI>(
|
internal data class BatchListState<K, Domain, UI>(
|
||||||
val uiBatches: List<Batch<K, List<UI>>> = emptyList(),
|
val uiBatches: List<Batch<K, List<UI>>>,
|
||||||
val processedItems: List<Batch<K, List<Domain>>>? = emptyList(),
|
val processedItems: List<Batch<K, List<Domain>>>,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal interface BatchItemConverter<Domain, UI> {
|
internal interface BatchItemConverter<Domain, UI> {
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,7 @@ internal class FeedComponentModel @Inject constructor(
|
||||||
NewsUMState.ERROR,
|
NewsUMState.ERROR,
|
||||||
-> emptyList()
|
-> emptyList()
|
||||||
},
|
},
|
||||||
|
paginationConfig = null,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onOpenAllNews = {
|
onOpenAllNews = {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.feed.model.feed
|
||||||
|
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.markets.TokenMarketParams
|
import com.tangem.domain.markets.TokenMarketParams
|
||||||
|
import com.tangem.domain.news.model.NewsListConfig
|
||||||
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -10,6 +11,17 @@ import com.tangem.features.feed.model.market.list.state.SortByTypeUM
|
||||||
internal interface FeedModelClickIntents {
|
internal interface FeedModelClickIntents {
|
||||||
fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency)
|
fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency)
|
||||||
fun onMarketOpenClick(sortBy: SortByTypeUM?)
|
fun onMarketOpenClick(sortBy: SortByTypeUM?)
|
||||||
fun onArticleClick(articleId: Int, preselectedArticlesId: List<Int> = emptyList())
|
|
||||||
|
/**
|
||||||
|
* Method to open article of news from different places.
|
||||||
|
* @param articleId - id of article, which should be opened.
|
||||||
|
* @param preselectedArticlesId - ids of articles, which were prefetched before opening news details.
|
||||||
|
* @param paginationConfig - config of current pagination from news list. Must be null, when open from other places.
|
||||||
|
*/
|
||||||
|
fun onArticleClick(
|
||||||
|
articleId: Int,
|
||||||
|
preselectedArticlesId: List<Int> = emptyList(),
|
||||||
|
paginationConfig: NewsListConfig? = null,
|
||||||
|
)
|
||||||
fun onOpenAllNews()
|
fun onOpenAllNews()
|
||||||
}
|
}
|
||||||
|
|
@ -244,7 +244,10 @@ internal class FeedMarketsBatchFlowManager(
|
||||||
|
|
||||||
fun reload(fiatPriceCurrency: String) {
|
fun reload(fiatPriceCurrency: String) {
|
||||||
modelScope.launch(dispatchers.default) {
|
modelScope.launch(dispatchers.default) {
|
||||||
stateManager.state.value = BatchListState()
|
stateManager.state.value = BatchListState(
|
||||||
|
processedItems = emptyList(),
|
||||||
|
uiBatches = emptyList(),
|
||||||
|
)
|
||||||
actionsFlow.emit(
|
actionsFlow.emit(
|
||||||
BatchAction.Reload(
|
BatchAction.Reload(
|
||||||
requestParams = TokenMarketListConfig(
|
requestParams = TokenMarketListConfig(
|
||||||
|
|
@ -315,9 +318,9 @@ internal class FeedMarketsBatchFlowManager(
|
||||||
|
|
||||||
fun getTokenMarketById(tokenId: CryptoCurrency.RawID): TokenMarket? {
|
fun getTokenMarketById(tokenId: CryptoCurrency.RawID): TokenMarket? {
|
||||||
return stateManager.state.value.processedItems
|
return stateManager.state.value.processedItems
|
||||||
?.asSequence()
|
.asSequence()
|
||||||
?.flatMap { it.data }
|
.flatMap { it.data }
|
||||||
?.firstOrNull { it.id == tokenId }
|
.firstOrNull { it.id == tokenId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -309,20 +309,22 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
||||||
snapshot = null,
|
snapshot = null,
|
||||||
tokenIds = listOf(params.token.id.value),
|
tokenIds = listOf(params.token.id.value),
|
||||||
),
|
),
|
||||||
).onRight { articles ->
|
).onRight { listOfArticles ->
|
||||||
state.update { marketsTokenDetailsUM ->
|
listOfArticles.collect { articles ->
|
||||||
val relatedNews = shortArticleToArticleConfigUMConverter.convert(articles)
|
state.update { marketsTokenDetailsUM ->
|
||||||
marketsTokenDetailsUM.copy(
|
val relatedNews = shortArticleToArticleConfigUMConverter.convert(articles)
|
||||||
relatedNews = marketsTokenDetailsUM.relatedNews.copy(
|
marketsTokenDetailsUM.copy(
|
||||||
articles = relatedNews,
|
relatedNews = marketsTokenDetailsUM.relatedNews.copy(
|
||||||
onArticledClicked = { articledId ->
|
articles = relatedNews,
|
||||||
params.onArticleClick(
|
onArticledClicked = { articledId ->
|
||||||
/* articledId */ articledId,
|
params.onArticleClick(
|
||||||
/* preselectedIds */ relatedNews.map { it.id },
|
/* articledId */ articledId,
|
||||||
)
|
/* preselectedIds */ relatedNews.map { it.id },
|
||||||
},
|
)
|
||||||
),
|
},
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.tangem.features.feed.model.news.details
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import arrow.core.getOrElse
|
import arrow.core.getOrElse
|
||||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
import com.tangem.core.decompose.model.ParamsContainer
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
|
|
@ -12,28 +11,22 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
|
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
|
||||||
import com.tangem.domain.markets.GetTokenPriceChartUseCase
|
import com.tangem.domain.markets.GetTokenPriceChartUseCase
|
||||||
import com.tangem.domain.markets.PriceChangeInterval
|
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
|
||||||
import com.tangem.domain.markets.TokenMarketParams
|
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
|
||||||
import com.tangem.domain.models.news.RelatedToken
|
|
||||||
import com.tangem.domain.news.usecase.MarkArticleAsViewedUseCase
|
import com.tangem.domain.news.usecase.MarkArticleAsViewedUseCase
|
||||||
import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase
|
import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase
|
||||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||||
|
import com.tangem.features.feed.model.news.details.factory.NewsDetailsIndexManager
|
||||||
import com.tangem.features.feed.model.news.details.converter.NewsDetailsConverter
|
import com.tangem.features.feed.model.news.details.converter.NewsDetailsConverter
|
||||||
import com.tangem.features.feed.model.news.details.converter.RelatedTokenConverter
|
import com.tangem.features.feed.model.news.details.loader.NewsRelatedTokensLoader
|
||||||
import com.tangem.features.feed.model.news.details.converter.TokenMarketInfoToParamsConverter
|
|
||||||
import com.tangem.features.feed.model.news.details.factory.NewsDetailsStateFactory
|
import com.tangem.features.feed.model.news.details.factory.NewsDetailsStateFactory
|
||||||
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
||||||
|
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
|
||||||
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
||||||
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
|
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import com.tangem.utils.extensions.indexOfFirstOrNull
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
@ -46,6 +39,7 @@ internal class NewsDetailsModel @Inject constructor(
|
||||||
override val dispatchers: CoroutineDispatcherProvider,
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||||
private val observeNewsDetailsUseCase: ObserveNewsDetailsUseCase,
|
private val observeNewsDetailsUseCase: ObserveNewsDetailsUseCase,
|
||||||
|
private val getNewsListBatchFlowUseCase: GetNewsListBatchFlowUseCase,
|
||||||
private val urlOpener: UrlOpener,
|
private val urlOpener: UrlOpener,
|
||||||
private val shareManager: ShareManager,
|
private val shareManager: ShareManager,
|
||||||
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||||
|
|
@ -56,10 +50,29 @@ internal class NewsDetailsModel @Inject constructor(
|
||||||
|
|
||||||
private val params = paramsContainer.require<DefaultNewsDetailsComponent.Params>()
|
private val params = paramsContainer.require<DefaultNewsDetailsComponent.Params>()
|
||||||
private val currentLanguage = Locale.getDefault().language
|
private val currentLanguage = Locale.getDefault().language
|
||||||
private val relatedTokensCache = mutableMapOf<Int, RelatedTokensUM>()
|
|
||||||
|
|
||||||
private val newsDetailsConverter = NewsDetailsConverter(onSourceClick = urlOpener::openUrl)
|
private val newsDetailsConverter = NewsDetailsConverter(onSourceClick = urlOpener::openUrl)
|
||||||
private val tokenMarketInfoToParamsConverter = TokenMarketInfoToParamsConverter()
|
|
||||||
|
private val paginationManager: NewsDetailsPaginationManager? = params.paginationConfig?.let { config ->
|
||||||
|
NewsDetailsPaginationManager(
|
||||||
|
getNewsListBatchFlowUseCase = getNewsListBatchFlowUseCase,
|
||||||
|
currentLanguage = Provider { config.language },
|
||||||
|
currentCategoryIds = Provider { config.categoryIds },
|
||||||
|
modelScope = modelScope,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
observeNewsDetailsUseCase = observeNewsDetailsUseCase,
|
||||||
|
prefetchedIds = params.preselectedArticlesId.toSet(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val relatedTokensLoader by lazy {
|
||||||
|
NewsRelatedTokensLoader(
|
||||||
|
getTokenMarketInfoUseCase = getTokenMarketInfoUseCase,
|
||||||
|
getTokenPriceChartUseCase = getTokenPriceChartUseCase,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
maxCount = RELATED_TOKEN_MAX_COUNT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||||
|
|
@ -73,8 +86,9 @@ internal class NewsDetailsModel @Inject constructor(
|
||||||
|
|
||||||
private val _state = MutableStateFlow(
|
private val _state = MutableStateFlow(
|
||||||
NewsDetailsUM(
|
NewsDetailsUM(
|
||||||
|
articlesStateUM = ArticlesStateUM.Loading,
|
||||||
articles = persistentListOf(),
|
articles = persistentListOf(),
|
||||||
selectedArticleIndex = 0,
|
selectedArticleIndex = -1,
|
||||||
onShareClick = {},
|
onShareClick = {},
|
||||||
onLikeClick = { /* [REDACTED_TODO_COMMENT] */ },
|
onLikeClick = { /* [REDACTED_TODO_COMMENT] */ },
|
||||||
onBackClick = params.onBackClicked,
|
onBackClick = params.onBackClicked,
|
||||||
|
|
@ -87,174 +101,100 @@ internal class NewsDetailsModel @Inject constructor(
|
||||||
currentStateProvider = Provider { _state.value },
|
currentStateProvider = Provider { _state.value },
|
||||||
shareManager = shareManager,
|
shareManager = shareManager,
|
||||||
onStateUpdate = { newState -> _state.update { newState } },
|
onStateUpdate = { newState -> _state.update { newState } },
|
||||||
|
onRetryClick = ::onRetryClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val state: StateFlow<NewsDetailsUM> = _state.asStateFlow()
|
val state: StateFlow<NewsDetailsUM> = _state.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (params.preselectedArticlesId.isNotEmpty()) {
|
loadNews()
|
||||||
handlePreselectedArticles()
|
|
||||||
} else {
|
|
||||||
// TODO handle pagination [REDACTED_TASK_KEY]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onArticleIndexChanged(newIndex: Int) {
|
private fun onArticleIndexChanged(newIndex: Int) {
|
||||||
stateFactory.updateSelectedArticleIndex(newIndex)
|
stateFactory.updateSelectedArticleIndex(newIndex)
|
||||||
val currentArticle = state.value.articles.getOrNull(newIndex)
|
val currentArticle = when (state.value.articlesStateUM) {
|
||||||
|
is ArticlesStateUM.Content -> {
|
||||||
|
paginationManager?.checkAndLoadMoreIfNeeded(
|
||||||
|
currentIndex = newIndex,
|
||||||
|
totalArticlesCount = state.value.articles.size,
|
||||||
|
)
|
||||||
|
state.value.articles.getOrNull(newIndex)
|
||||||
|
}
|
||||||
|
ArticlesStateUM.Loading,
|
||||||
|
is ArticlesStateUM.LoadingError,
|
||||||
|
-> null
|
||||||
|
}
|
||||||
currentArticle?.let { article ->
|
currentArticle?.let { article ->
|
||||||
markArticleAsViewed(article.id)
|
markArticleAsViewed(article.id)
|
||||||
loadRelatedTokens(article)
|
loadRelatedTokens(article)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePreselectedArticles() {
|
private fun loadNews() {
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
observeNewsDetailsUseCase.prefetch(
|
initialPrefetch()
|
||||||
newsIds = params.preselectedArticlesId,
|
paginationManager?.start()
|
||||||
language = currentLanguage,
|
val articleIdsFlow = paginationManager?.cachedPrefetchedIds ?: flowOf(params.preselectedArticlesId)
|
||||||
)
|
combine(
|
||||||
|
articleIdsFlow,
|
||||||
observeNewsDetailsUseCase
|
observeNewsDetailsUseCase.invoke(),
|
||||||
.invoke()
|
) { articleIds, articlesMap ->
|
||||||
.map { articlesMap ->
|
articleIds.mapNotNull { articleId ->
|
||||||
params.preselectedArticlesId.mapNotNull { articleId ->
|
articlesMap[articleId]?.let { detailedArticle ->
|
||||||
articlesMap[articleId]?.let { detailedArticle ->
|
newsDetailsConverter.convert(detailedArticle)
|
||||||
newsDetailsConverter.convert(detailedArticle)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.map { articles ->
|
.map { articles ->
|
||||||
articles.toImmutableList()
|
articles.toImmutableList()
|
||||||
}
|
}
|
||||||
.onEach { articles ->
|
.onEach { newArticles ->
|
||||||
val selectedIndex = articles.indexOfFirstOrNull { it.id == params.articleId } ?: 0
|
val currentState = _state.value
|
||||||
stateFactory.updateArticles(articles, selectedIndex)
|
val newIndex = NewsDetailsIndexManager.calculateNewIndex(
|
||||||
|
currentState = NewsDetailsIndexManager.NewsDetailsState(
|
||||||
|
articles = currentState.articles,
|
||||||
|
selectedArticleIndex = currentState.selectedArticleIndex,
|
||||||
|
),
|
||||||
|
newArticles = newArticles,
|
||||||
|
defaultArticleId = params.articleId,
|
||||||
|
)
|
||||||
|
stateFactory.updateArticles(newArticles, newIndex)
|
||||||
}
|
}
|
||||||
.launchIn(modelScope)
|
.launchIn(modelScope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadRelatedTokens(article: ArticleUM) {
|
private suspend fun initialPrefetch() {
|
||||||
modelScope.launch(dispatchers.default) {
|
observeNewsDetailsUseCase.prefetch(
|
||||||
val cachedTokens = getCachedRelatedTokens(article.id)
|
newsIds = params.preselectedArticlesId,
|
||||||
if (cachedTokens != null) {
|
language = currentLanguage,
|
||||||
stateFactory.updateRelatedTokens(cachedTokens)
|
).mapLeft {
|
||||||
return@launch
|
stateFactory.createErrorState()
|
||||||
}
|
|
||||||
|
|
||||||
stateFactory.updateRelatedTokens(RelatedTokensUM.Loading)
|
|
||||||
|
|
||||||
val relatedTokens = article.relatedTokens.take(RELATED_TOKEN_MAX_COUNT)
|
|
||||||
if (relatedTokens.isEmpty()) {
|
|
||||||
handleEmptyRelatedTokens(article.id)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val appCurrency = currentAppCurrency.value
|
|
||||||
val tokenDataList = loadTokensData(relatedTokens, appCurrency)
|
|
||||||
|
|
||||||
if (tokenDataList.isEmpty()) {
|
|
||||||
handleEmptyTokenData(article.id)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val resultState = createRelatedTokensState(tokenDataList, appCurrency)
|
|
||||||
relatedTokensCache[article.id] = resultState
|
|
||||||
stateFactory.updateRelatedTokens(resultState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCachedRelatedTokens(articleId: Int): RelatedTokensUM? {
|
private fun onRetryClicked() {
|
||||||
return relatedTokensCache[articleId]?.takeIf { it !is RelatedTokensUM.Loading }
|
stateFactory.createLoadingState()
|
||||||
|
modelScope.launch(dispatchers.default) {
|
||||||
|
initialPrefetch()
|
||||||
|
paginationManager?.reload()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEmptyRelatedTokens(articleId: Int) {
|
private fun loadRelatedTokens(article: ArticleUM) {
|
||||||
val errorState = RelatedTokensUM.LoadingError
|
modelScope.launch(dispatchers.default) {
|
||||||
relatedTokensCache[articleId] = errorState
|
stateFactory.updateRelatedTokens(RelatedTokensUM.Loading)
|
||||||
stateFactory.updateRelatedTokens(errorState)
|
val appCurrency = currentAppCurrency.value
|
||||||
}
|
val resultState = relatedTokensLoader.load(
|
||||||
|
articleId = article.id,
|
||||||
private suspend fun CoroutineScope.loadTokensData(
|
relatedTokens = article.relatedTokens,
|
||||||
relatedTokens: List<RelatedToken>,
|
appCurrency = appCurrency,
|
||||||
appCurrency: AppCurrency,
|
onTokenClick = { tokenParams, currency ->
|
||||||
): List<Pair<MarketsListItemUM, TokenMarketParams>> {
|
params.onTokenClick(tokenParams, currency)
|
||||||
val relatedTokenConverter = RelatedTokenConverter(appCurrency = appCurrency)
|
},
|
||||||
|
)
|
||||||
return relatedTokens.map { token ->
|
stateFactory.updateRelatedTokens(resultState)
|
||||||
async(dispatchers.default) {
|
|
||||||
loadSingleTokenData(token, appCurrency, relatedTokenConverter)
|
|
||||||
}
|
|
||||||
}.awaitAll().filterNotNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadSingleTokenData(
|
|
||||||
token: RelatedToken,
|
|
||||||
appCurrency: AppCurrency,
|
|
||||||
relatedTokenConverter: RelatedTokenConverter,
|
|
||||||
): Pair<MarketsListItemUM, TokenMarketParams>? {
|
|
||||||
val tokenId = CryptoCurrency.RawID(token.id)
|
|
||||||
val tokenInfoResult = getTokenMarketInfoUseCase(
|
|
||||||
appCurrency = appCurrency,
|
|
||||||
tokenId = tokenId,
|
|
||||||
tokenSymbol = token.symbol,
|
|
||||||
)
|
|
||||||
|
|
||||||
return tokenInfoResult.fold(
|
|
||||||
ifLeft = { null },
|
|
||||||
ifRight = { tokenInfo ->
|
|
||||||
val chart = loadTokenChart(tokenId, token.symbol, appCurrency)
|
|
||||||
val tokenItem = relatedTokenConverter.convert(tokenInfo to chart)
|
|
||||||
val tokenParams = tokenMarketInfoToParamsConverter.convert(tokenInfo)
|
|
||||||
tokenItem to tokenParams
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun loadTokenChart(
|
|
||||||
tokenId: CryptoCurrency.RawID,
|
|
||||||
tokenSymbol: String,
|
|
||||||
appCurrency: AppCurrency,
|
|
||||||
): com.tangem.domain.markets.TokenChart? {
|
|
||||||
val chartResult = getTokenPriceChartUseCase(
|
|
||||||
appCurrency = appCurrency,
|
|
||||||
interval = PriceChangeInterval.H24,
|
|
||||||
tokenId = tokenId,
|
|
||||||
tokenSymbol = tokenSymbol,
|
|
||||||
preview = true,
|
|
||||||
)
|
|
||||||
return chartResult.getOrElse { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleEmptyTokenData(articleId: Int) {
|
|
||||||
val errorState = RelatedTokensUM.LoadingError
|
|
||||||
relatedTokensCache[articleId] = errorState
|
|
||||||
stateFactory.updateRelatedTokens(errorState)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createRelatedTokensState(
|
|
||||||
tokenDataList: List<Pair<MarketsListItemUM, TokenMarketParams>>,
|
|
||||||
appCurrency: AppCurrency,
|
|
||||||
): RelatedTokensUM.Content {
|
|
||||||
val tokenItems = tokenDataList.map { it.first }
|
|
||||||
val onTokenClick = createTokenClickHandler(tokenDataList, appCurrency)
|
|
||||||
|
|
||||||
return stateFactory.createRelatedTokensContent(
|
|
||||||
items = tokenItems.toImmutableList(),
|
|
||||||
onTokenClick = onTokenClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createTokenClickHandler(
|
|
||||||
tokenDataList: List<Pair<MarketsListItemUM, TokenMarketParams>>,
|
|
||||||
appCurrency: AppCurrency,
|
|
||||||
): (MarketsListItemUM) -> Unit {
|
|
||||||
return { item ->
|
|
||||||
val tokenData = tokenDataList.find { it.first.id == item.id }
|
|
||||||
tokenData?.second?.let { tokenParams ->
|
|
||||||
params.onTokenClick(tokenParams, appCurrency)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
package com.tangem.features.feed.model.news.details
|
||||||
|
|
||||||
|
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
|
||||||
|
import com.tangem.domain.news.usecase.ObserveNewsDetailsUseCase
|
||||||
|
import com.tangem.features.feed.model.news.list.statemanager.NewsListBatchFlowManager
|
||||||
|
import com.tangem.pagination.BatchFetchResult
|
||||||
|
import com.tangem.pagination.PaginationStatus
|
||||||
|
import com.tangem.utils.Provider
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
internal class NewsDetailsPaginationManager(
|
||||||
|
private val observeNewsDetailsUseCase: ObserveNewsDetailsUseCase,
|
||||||
|
private val currentLanguage: Provider<String>,
|
||||||
|
getNewsListBatchFlowUseCase: GetNewsListBatchFlowUseCase,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
currentCategoryIds: Provider<List<Int>>,
|
||||||
|
modelScope: CoroutineScope,
|
||||||
|
prefetchedIds: Set<Int>,
|
||||||
|
) : NewsListBatchFlowManager(
|
||||||
|
getNewsListBatchFlowUseCase = getNewsListBatchFlowUseCase,
|
||||||
|
currentLanguage = currentLanguage,
|
||||||
|
currentCategoryIds = currentCategoryIds,
|
||||||
|
modelScope = modelScope,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val _cachedPrefetchedIds = MutableStateFlow(prefetchedIds)
|
||||||
|
val cachedPrefetchedIds = _cachedPrefetchedIds.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
modelScope.launch(dispatchers.default) {
|
||||||
|
combine(
|
||||||
|
rawArticlesFlow,
|
||||||
|
observeNewsDetailsUseCase(),
|
||||||
|
_cachedPrefetchedIds,
|
||||||
|
) { articles, cachedArticles, prefetched ->
|
||||||
|
calculateIdsToFetch(
|
||||||
|
visibleIds = articles.map { it.id }.toSet(),
|
||||||
|
cachedIds = cachedArticles.keys,
|
||||||
|
alreadyPrefetched = prefetched,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { newIds ->
|
||||||
|
if (newIds.isNotEmpty()) {
|
||||||
|
observeNewsDetailsUseCase.prefetch(
|
||||||
|
newsIds = newIds,
|
||||||
|
language = currentLanguage(),
|
||||||
|
)
|
||||||
|
_cachedPrefetchedIds.update { it + newIds }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateIdsToFetch(visibleIds: Set<Int>, cachedIds: Set<Int>, alreadyPrefetched: Set<Int>): Set<Int> {
|
||||||
|
return visibleIds - cachedIds - alreadyPrefetched
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkAndLoadMoreIfNeeded(
|
||||||
|
currentIndex: Int,
|
||||||
|
totalArticlesCount: Int,
|
||||||
|
preloadThreshold: Int = PRELOAD_THRESHOLD,
|
||||||
|
) {
|
||||||
|
if (currentIndex >= totalArticlesCount - preloadThreshold) {
|
||||||
|
loadMoreIfPossible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadMoreIfPossible() {
|
||||||
|
modelScope.launch(dispatchers.default) {
|
||||||
|
when (val status = paginationStatus.value) {
|
||||||
|
is PaginationStatus.InitialLoading,
|
||||||
|
is PaginationStatus.NextBatchLoading,
|
||||||
|
is PaginationStatus.EndOfPagination,
|
||||||
|
-> {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
is PaginationStatus.InitialLoadingError -> {
|
||||||
|
reload()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
is PaginationStatus.Paginating -> {
|
||||||
|
val lastResult = status.lastResult
|
||||||
|
val canLoadMore = lastResult is BatchFetchResult.Success && !lastResult.last
|
||||||
|
|
||||||
|
if (canLoadMore) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PaginationStatus.None -> {
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PRELOAD_THRESHOLD = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.tangem.features.feed.model.news.details.factory
|
||||||
|
|
||||||
|
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
||||||
|
import com.tangem.utils.extensions.indexOfFirstOrNull
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
internal object NewsDetailsIndexManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate new index of article, trying to save current selected article while is loading
|
||||||
|
*/
|
||||||
|
fun calculateNewIndex(
|
||||||
|
currentState: NewsDetailsState,
|
||||||
|
newArticles: ImmutableList<ArticleUM>,
|
||||||
|
defaultArticleId: Int,
|
||||||
|
): Int {
|
||||||
|
val currentArticleId = if (currentState.articles.isEmpty()) {
|
||||||
|
defaultArticleId
|
||||||
|
} else {
|
||||||
|
currentState.articles.getOrNull(currentState.selectedArticleIndex)?.id
|
||||||
|
?: defaultArticleId
|
||||||
|
}
|
||||||
|
return newArticles.indexOfFirstOrNull { it.id == currentArticleId } ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NewsDetailsState(
|
||||||
|
val articles: ImmutableList<ArticleUM>,
|
||||||
|
val selectedArticleIndex: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package com.tangem.features.feed.model.news.details.factory
|
package com.tangem.features.feed.model.news.details.factory
|
||||||
|
|
||||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
|
||||||
import com.tangem.core.navigation.share.ShareManager
|
import com.tangem.core.navigation.share.ShareManager
|
||||||
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
||||||
|
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
|
||||||
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
||||||
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
|
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
|
|
@ -12,6 +12,7 @@ internal class NewsDetailsStateFactory(
|
||||||
private val currentStateProvider: Provider<NewsDetailsUM>,
|
private val currentStateProvider: Provider<NewsDetailsUM>,
|
||||||
private val shareManager: ShareManager,
|
private val shareManager: ShareManager,
|
||||||
private val onStateUpdate: (NewsDetailsUM) -> Unit,
|
private val onStateUpdate: (NewsDetailsUM) -> Unit,
|
||||||
|
private val onRetryClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun updateArticles(articles: List<ArticleUM>, selectedIndex: Int) {
|
fun updateArticles(articles: List<ArticleUM>, selectedIndex: Int) {
|
||||||
|
|
@ -20,6 +21,7 @@ internal class NewsDetailsStateFactory(
|
||||||
onStateUpdate(
|
onStateUpdate(
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
articles = articles.toImmutableList(),
|
articles = articles.toImmutableList(),
|
||||||
|
articlesStateUM = ArticlesStateUM.Content,
|
||||||
selectedArticleIndex = selectedIndex,
|
selectedArticleIndex = selectedIndex,
|
||||||
onShareClick = {
|
onShareClick = {
|
||||||
currentArticle?.let {
|
currentArticle?.let {
|
||||||
|
|
@ -50,13 +52,13 @@ internal class NewsDetailsStateFactory(
|
||||||
onStateUpdate(currentState.copy(relatedTokensUM = relatedTokens))
|
onStateUpdate(currentState.copy(relatedTokensUM = relatedTokens))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createRelatedTokensContent(
|
fun createErrorState() {
|
||||||
items: List<MarketsListItemUM>,
|
val currentState = currentStateProvider()
|
||||||
onTokenClick: (MarketsListItemUM) -> Unit,
|
onStateUpdate(currentState.copy(articlesStateUM = ArticlesStateUM.LoadingError(onRetryClick)))
|
||||||
): RelatedTokensUM.Content {
|
}
|
||||||
return RelatedTokensUM.Content(
|
|
||||||
items = items.toImmutableList(),
|
fun createLoadingState() {
|
||||||
onTokenClick = onTokenClick,
|
val currentState = currentStateProvider()
|
||||||
)
|
onStateUpdate(currentState.copy(articlesStateUM = ArticlesStateUM.Loading))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
package com.tangem.features.feed.model.news.details.loader
|
||||||
|
|
||||||
|
import arrow.core.getOrElse
|
||||||
|
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||||
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
|
import com.tangem.domain.markets.*
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.news.RelatedToken
|
||||||
|
import com.tangem.features.feed.model.news.details.converter.RelatedTokenConverter
|
||||||
|
import com.tangem.features.feed.model.news.details.converter.TokenMarketInfoToParamsConverter
|
||||||
|
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
internal class NewsRelatedTokensLoader(
|
||||||
|
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
|
||||||
|
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
private val maxCount: Int,
|
||||||
|
) {
|
||||||
|
private val relatedTokensCache = mutableMapOf<Int, RelatedTokensUM>()
|
||||||
|
private val tokenMarketInfoToParamsConverter = TokenMarketInfoToParamsConverter()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads data about related tokens.
|
||||||
|
* Returns the cached result if available, otherwise loads the data.
|
||||||
|
*
|
||||||
|
* @param articleId article ID for caching
|
||||||
|
* @param relatedTokens list of related tokens
|
||||||
|
* @param appCurrency application currency
|
||||||
|
* @param onTokenClick token click handler
|
||||||
|
* @return state of related tokens
|
||||||
|
*/
|
||||||
|
suspend fun load(
|
||||||
|
articleId: Int,
|
||||||
|
relatedTokens: List<RelatedToken>,
|
||||||
|
appCurrency: AppCurrency,
|
||||||
|
onTokenClick: (TokenMarketParams, AppCurrency) -> Unit,
|
||||||
|
): RelatedTokensUM = withContext(dispatchers.default) {
|
||||||
|
val cached = getCachedRelatedTokens(articleId)
|
||||||
|
if (cached != null) {
|
||||||
|
return@withContext cached
|
||||||
|
}
|
||||||
|
|
||||||
|
val tokensToLoad = relatedTokens.take(maxCount)
|
||||||
|
if (tokensToLoad.isEmpty()) {
|
||||||
|
val errorState = RelatedTokensUM.LoadingError
|
||||||
|
relatedTokensCache[articleId] = errorState
|
||||||
|
return@withContext errorState
|
||||||
|
}
|
||||||
|
|
||||||
|
val tokenDataList = loadTokensData(tokensToLoad, appCurrency)
|
||||||
|
|
||||||
|
if (tokenDataList.isEmpty()) {
|
||||||
|
val errorState = RelatedTokensUM.LoadingError
|
||||||
|
relatedTokensCache[articleId] = errorState
|
||||||
|
return@withContext errorState
|
||||||
|
}
|
||||||
|
|
||||||
|
val resultState = createRelatedTokensState(tokenDataList, appCurrency, onTokenClick)
|
||||||
|
relatedTokensCache[articleId] = resultState
|
||||||
|
resultState
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCachedRelatedTokens(articleId: Int): RelatedTokensUM? {
|
||||||
|
return relatedTokensCache[articleId]?.takeIf { it !is RelatedTokensUM.Loading }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun CoroutineScope.loadTokensData(
|
||||||
|
relatedTokens: List<RelatedToken>,
|
||||||
|
appCurrency: AppCurrency,
|
||||||
|
): List<Pair<MarketsListItemUM, TokenMarketParams>> {
|
||||||
|
val relatedTokenConverter = RelatedTokenConverter(appCurrency = appCurrency)
|
||||||
|
|
||||||
|
return relatedTokens.map { token ->
|
||||||
|
async(dispatchers.default) {
|
||||||
|
loadSingleTokenData(token, appCurrency, relatedTokenConverter)
|
||||||
|
}
|
||||||
|
}.awaitAll().filterNotNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadSingleTokenData(
|
||||||
|
token: RelatedToken,
|
||||||
|
appCurrency: AppCurrency,
|
||||||
|
relatedTokenConverter: RelatedTokenConverter,
|
||||||
|
): Pair<MarketsListItemUM, TokenMarketParams>? {
|
||||||
|
val tokenId = CryptoCurrency.RawID(token.id)
|
||||||
|
val tokenInfoResult = getTokenMarketInfoUseCase(
|
||||||
|
appCurrency = appCurrency,
|
||||||
|
tokenId = tokenId,
|
||||||
|
tokenSymbol = token.symbol,
|
||||||
|
)
|
||||||
|
|
||||||
|
return tokenInfoResult.fold(
|
||||||
|
ifLeft = { null },
|
||||||
|
ifRight = { tokenInfo ->
|
||||||
|
val chart = loadTokenChart(tokenId, token.symbol, appCurrency)
|
||||||
|
val tokenItem = relatedTokenConverter.convert(tokenInfo to chart)
|
||||||
|
val tokenParams = tokenMarketInfoToParamsConverter.convert(tokenInfo)
|
||||||
|
tokenItem to tokenParams
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadTokenChart(
|
||||||
|
tokenId: CryptoCurrency.RawID,
|
||||||
|
tokenSymbol: String,
|
||||||
|
appCurrency: AppCurrency,
|
||||||
|
): TokenChart? {
|
||||||
|
val chartResult = getTokenPriceChartUseCase(
|
||||||
|
appCurrency = appCurrency,
|
||||||
|
interval = PriceChangeInterval.H24,
|
||||||
|
tokenId = tokenId,
|
||||||
|
tokenSymbol = tokenSymbol,
|
||||||
|
preview = true,
|
||||||
|
)
|
||||||
|
return chartResult.getOrElse { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRelatedTokensState(
|
||||||
|
tokenDataList: List<Pair<MarketsListItemUM, TokenMarketParams>>,
|
||||||
|
appCurrency: AppCurrency,
|
||||||
|
onTokenClick: (TokenMarketParams, AppCurrency) -> Unit,
|
||||||
|
): RelatedTokensUM.Content {
|
||||||
|
val tokenItems = tokenDataList.map { it.first }
|
||||||
|
val onTokenClickHandler = createTokenClickHandler(tokenDataList, appCurrency, onTokenClick)
|
||||||
|
|
||||||
|
return RelatedTokensUM.Content(
|
||||||
|
items = tokenItems.toImmutableList(),
|
||||||
|
onTokenClick = onTokenClickHandler,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTokenClickHandler(
|
||||||
|
tokenDataList: List<Pair<MarketsListItemUM, TokenMarketParams>>,
|
||||||
|
appCurrency: AppCurrency,
|
||||||
|
onTokenClick: (TokenMarketParams, AppCurrency) -> Unit,
|
||||||
|
): (MarketsListItemUM) -> Unit {
|
||||||
|
return { item ->
|
||||||
|
val tokenData = tokenDataList.find { it.first.id == item.id }
|
||||||
|
tokenData?.second?.let { tokenParams ->
|
||||||
|
onTokenClick(tokenParams, appCurrency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,25 +1,24 @@
|
||||||
package com.tangem.features.feed.model.news.list
|
package com.tangem.features.feed.model.news.list
|
||||||
|
|
||||||
import com.tangem.common.ui.R
|
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
import com.tangem.core.decompose.model.ParamsContainer
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
import com.tangem.core.ui.components.chip.entity.ChipUM
|
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
import com.tangem.domain.news.model.NewsListConfig
|
||||||
import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase
|
import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase
|
||||||
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
|
import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase
|
||||||
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
|
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
|
||||||
|
import com.tangem.features.feed.model.news.list.calculator.NewsListStateManager
|
||||||
|
import com.tangem.features.feed.model.news.list.loader.NewsCategoriesLoader
|
||||||
import com.tangem.features.feed.model.news.list.statemanager.NewsListBatchFlowManager
|
import com.tangem.features.feed.model.news.list.statemanager.NewsListBatchFlowManager
|
||||||
import com.tangem.features.feed.ui.news.list.state.NewsListState
|
import com.tangem.features.feed.ui.news.list.state.NewsListState
|
||||||
import com.tangem.features.feed.ui.news.list.state.NewsListUM
|
import com.tangem.features.feed.ui.news.list.state.NewsListUM
|
||||||
import com.tangem.pagination.PaginationStatus
|
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
import com.tangem.utils.SupportedLanguages
|
import com.tangem.utils.SupportedLanguages
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
@ -39,6 +38,14 @@ internal class NewsListModel @Inject constructor(
|
||||||
private val selectedCategoryId = MutableStateFlow<Int?>(null)
|
private val selectedCategoryId = MutableStateFlow<Int?>(null)
|
||||||
private val currentLanguage = SupportedLanguages.getCurrentSupportedLanguageCode()
|
private val currentLanguage = SupportedLanguages.getCurrentSupportedLanguageCode()
|
||||||
|
|
||||||
|
private val categoriesLoader by lazy {
|
||||||
|
NewsCategoriesLoader(
|
||||||
|
getNewsCategoriesUseCase = getNewsCategoriesUseCase,
|
||||||
|
defaultAllNewsCategoryId = DEFAULT_ALL_NEWS_CATEGORIES_ID,
|
||||||
|
onCategoryClick = ::onCategoryClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val batchFlowManager by lazy {
|
private val batchFlowManager by lazy {
|
||||||
NewsListBatchFlowManager(
|
NewsListBatchFlowManager(
|
||||||
getNewsListBatchFlowUseCase = getNewsListBatchFlowUseCase,
|
getNewsListBatchFlowUseCase = getNewsListBatchFlowUseCase,
|
||||||
|
|
@ -61,6 +68,7 @@ internal class NewsListModel @Inject constructor(
|
||||||
params.onArticleClicked(
|
params.onArticleClicked(
|
||||||
/* currentArticle */ articleId,
|
/* currentArticle */ articleId,
|
||||||
/* prefetchedArticles */ getCurrentFetchedArticlesIds(),
|
/* prefetchedArticles */ getCurrentFetchedArticlesIds(),
|
||||||
|
/* paginationConfig */ createNewsListConfig(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onBackClick = params.onBackClick,
|
onBackClick = params.onBackClick,
|
||||||
|
|
@ -76,31 +84,7 @@ internal class NewsListModel @Inject constructor(
|
||||||
|
|
||||||
private fun loadCategories() {
|
private fun loadCategories() {
|
||||||
modelScope.launch(dispatchers.default) {
|
modelScope.launch(dispatchers.default) {
|
||||||
val allCategoriesChip = ChipUM(
|
val filterChips = categoriesLoader.load()
|
||||||
id = DEFAULT_ALL_NEWS_CATEGORIES_ID,
|
|
||||||
text = TextReference.Res(R.string.news_all_news),
|
|
||||||
isSelected = true,
|
|
||||||
onClick = { onCategoryClick(DEFAULT_ALL_NEWS_CATEGORIES_ID) },
|
|
||||||
)
|
|
||||||
val filterChips = getNewsCategoriesUseCase
|
|
||||||
.invoke()
|
|
||||||
.fold(
|
|
||||||
ifLeft = {
|
|
||||||
persistentListOf()
|
|
||||||
},
|
|
||||||
ifRight = { categories ->
|
|
||||||
(listOf(allCategoriesChip) + categories.map { articleCategory ->
|
|
||||||
ChipUM(
|
|
||||||
id = articleCategory.id,
|
|
||||||
text = TextReference.Str(articleCategory.name),
|
|
||||||
isSelected = false,
|
|
||||||
onClick = {
|
|
||||||
onCategoryClick(articleCategory.id)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}).toPersistentList()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
_state.update { currentState ->
|
_state.update { currentState ->
|
||||||
currentState.copy(filters = filterChips)
|
currentState.copy(filters = filterChips)
|
||||||
}
|
}
|
||||||
|
|
@ -114,23 +98,16 @@ internal class NewsListModel @Inject constructor(
|
||||||
batchFlowManager.isInInitialLoadingErrorState,
|
batchFlowManager.isInInitialLoadingErrorState,
|
||||||
batchFlowManager.paginationStatus,
|
batchFlowManager.paginationStatus,
|
||||||
) { articles, isError, paginationStatus ->
|
) { articles, isError, paginationStatus ->
|
||||||
when {
|
NewsListStateManager.calculateState(
|
||||||
isError -> NewsListState.LoadingError(
|
articles = articles,
|
||||||
onRetryClicked = {
|
isError = isError,
|
||||||
loadCategories()
|
paginationStatus = paginationStatus,
|
||||||
batchFlowManager.reload()
|
onRetryClick = {
|
||||||
},
|
loadCategories()
|
||||||
) to persistentListOf()
|
batchFlowManager.reload()
|
||||||
paginationStatus is PaginationStatus.InitialLoading && articles.isEmpty() -> {
|
},
|
||||||
NewsListState.Loading to persistentListOf()
|
onLoadMore = { batchFlowManager.loadMore() },
|
||||||
}
|
)
|
||||||
articles.isEmpty() -> {
|
|
||||||
NewsListState.Loading to persistentListOf()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
NewsListState.Content(loadMore = { batchFlowManager.loadMore() }) to articles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.collect { (listState, articles) ->
|
}.collect { (listState, articles) ->
|
||||||
_state.update { currentState ->
|
_state.update { currentState ->
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
|
|
@ -168,6 +145,15 @@ internal class NewsListModel @Inject constructor(
|
||||||
return state.value.listOfArticles.map { it.id }
|
return state.value.listOfArticles.map { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createNewsListConfig(): NewsListConfig {
|
||||||
|
return NewsListConfig(
|
||||||
|
language = currentLanguage,
|
||||||
|
snapshot = null,
|
||||||
|
tokenIds = emptyList(),
|
||||||
|
categoryIds = selectedCategoryId.value?.takeIf { it > 0 }?.let { listOf(it) }.orEmpty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DEFAULT_ALL_NEWS_CATEGORIES_ID = -1
|
private const val DEFAULT_ALL_NEWS_CATEGORIES_ID = -1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.tangem.features.feed.model.news.list.calculator
|
||||||
|
|
||||||
|
import com.tangem.common.ui.news.ArticleConfigUM
|
||||||
|
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 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 paginationStatus the current pagination status
|
||||||
|
* @param onRetryClick handler for the retry loading action
|
||||||
|
* @param onLoadMore handler for loading the next page
|
||||||
|
* @return a pair: the list state and the list of articles to display
|
||||||
|
*/
|
||||||
|
fun calculateState(
|
||||||
|
articles: ImmutableList<ArticleConfigUM>,
|
||||||
|
isError: Boolean,
|
||||||
|
paginationStatus: PaginationStatus<*>,
|
||||||
|
onRetryClick: () -> Unit,
|
||||||
|
onLoadMore: () -> Unit,
|
||||||
|
): Pair<NewsListState, ImmutableList<ArticleConfigUM>> {
|
||||||
|
return when {
|
||||||
|
isError -> NewsListState.LoadingError(onRetryClick) to persistentListOf()
|
||||||
|
paginationStatus is PaginationStatus.InitialLoading && articles.isEmpty() -> {
|
||||||
|
NewsListState.Loading to persistentListOf()
|
||||||
|
}
|
||||||
|
articles.isEmpty() -> NewsListState.Loading to persistentListOf()
|
||||||
|
else -> NewsListState.Content(loadMore = onLoadMore) to articles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.tangem.features.feed.model.news.list.loader
|
||||||
|
|
||||||
|
import com.tangem.common.ui.R
|
||||||
|
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.domain.models.news.ArticleCategory
|
||||||
|
import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
|
internal class NewsCategoriesLoader(
|
||||||
|
private val getNewsCategoriesUseCase: GetNewsCategoriesUseCase,
|
||||||
|
private val defaultAllNewsCategoryId: Int,
|
||||||
|
private val onCategoryClick: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun load(): ImmutableList<ChipUM> {
|
||||||
|
val allCategoriesChip = createAllNewsChip()
|
||||||
|
val categoriesResult = getNewsCategoriesUseCase.invoke()
|
||||||
|
|
||||||
|
return categoriesResult.fold(
|
||||||
|
ifLeft = { persistentListOf(allCategoriesChip) },
|
||||||
|
ifRight = { categories ->
|
||||||
|
val categoryChips = categories.map { articleCategory ->
|
||||||
|
createCategoryChip(articleCategory)
|
||||||
|
}
|
||||||
|
(listOf(allCategoriesChip) + categoryChips).toPersistentList()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAllNewsChip(): ChipUM {
|
||||||
|
return ChipUM(
|
||||||
|
id = defaultAllNewsCategoryId,
|
||||||
|
text = TextReference.Res(R.string.news_all_news),
|
||||||
|
isSelected = true,
|
||||||
|
onClick = { onCategoryClick(defaultAllNewsCategoryId) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCategoryChip(articleCategory: ArticleCategory): ChipUM {
|
||||||
|
return ChipUM(
|
||||||
|
id = articleCategory.id,
|
||||||
|
text = TextReference.Str(articleCategory.name),
|
||||||
|
isSelected = false,
|
||||||
|
onClick = { onCategoryClick(articleCategory.id) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,17 +14,18 @@ import com.tangem.utils.Provider
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
internal class NewsListBatchFlowManager(
|
internal open class NewsListBatchFlowManager(
|
||||||
getNewsListBatchFlowUseCase: GetNewsListBatchFlowUseCase,
|
getNewsListBatchFlowUseCase: GetNewsListBatchFlowUseCase,
|
||||||
private val currentLanguage: Provider<String>,
|
private val currentLanguage: Provider<String>,
|
||||||
private val currentCategoryIds: Provider<List<Int>>,
|
private val currentCategoryIds: Provider<List<Int>>,
|
||||||
private val modelScope: CoroutineScope,
|
protected val modelScope: CoroutineScope,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
protected val dispatchers: CoroutineDispatcherProvider,
|
||||||
) {
|
) {
|
||||||
private val actionsFlow = MutableSharedFlow<BatchAction<Int, NewsListConfig, Nothing>>()
|
private val actionsFlow = MutableSharedFlow<BatchAction<Int, NewsListConfig, Nothing>>()
|
||||||
private val converter by lazy {
|
private val converter by lazy {
|
||||||
|
|
@ -41,6 +42,19 @@ internal class NewsListBatchFlowManager(
|
||||||
|
|
||||||
private val resultBatches = MutableStateFlow<List<Batch<Int, List<ArticleConfigUM>>>>(emptyList())
|
private val resultBatches = MutableStateFlow<List<Batch<Int, List<ArticleConfigUM>>>>(emptyList())
|
||||||
|
|
||||||
|
val rawArticlesFlow: StateFlow<ImmutableList<ShortArticle>> = batchFlow.state
|
||||||
|
.map { batchListState ->
|
||||||
|
batchListState.data
|
||||||
|
.flatMap { batch -> batch.data }
|
||||||
|
.toImmutableList()
|
||||||
|
}
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.stateIn(
|
||||||
|
scope = modelScope,
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = persistentListOf(),
|
||||||
|
)
|
||||||
|
|
||||||
val uiItems: StateFlow<ImmutableList<ArticleConfigUM>>
|
val uiItems: StateFlow<ImmutableList<ArticleConfigUM>>
|
||||||
get() = batchFlow.state
|
get() = batchFlow.state
|
||||||
.map { batchListState ->
|
.map { batchListState ->
|
||||||
|
|
@ -64,15 +78,14 @@ internal class NewsListBatchFlowManager(
|
||||||
initialValue = false,
|
initialValue = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
val paginationStatus: StateFlow<PaginationStatus<List<ShortArticle>>>
|
val paginationStatus: StateFlow<PaginationStatus<List<ShortArticle>>> = batchFlow.state
|
||||||
get() = batchFlow.state
|
.map { it.status }
|
||||||
.map { it.status }
|
.distinctUntilChanged()
|
||||||
.distinctUntilChanged()
|
.stateIn(
|
||||||
.stateIn(
|
scope = modelScope,
|
||||||
scope = modelScope,
|
started = SharingStarted.Eagerly,
|
||||||
started = SharingStarted.Eagerly,
|
initialValue = PaginationStatus.InitialLoading,
|
||||||
initialValue = PaginationStatus.InitialLoading,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
batchFlow.state
|
batchFlow.state
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.ripple
|
import androidx.compose.material3.ripple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -74,7 +71,7 @@ internal fun FeedList(state: FeedListUM, modifier: Modifier = Modifier) {
|
||||||
) { animatedState ->
|
) { animatedState ->
|
||||||
when (animatedState) {
|
when (animatedState) {
|
||||||
is GlobalFeedState.Loading -> {
|
is GlobalFeedState.Loading -> {
|
||||||
FeeListLoading(
|
FeedListLoading(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
|
|
@ -88,7 +85,7 @@ internal fun FeedList(state: FeedListUM, modifier: Modifier = Modifier) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is GlobalFeedState.Content -> {
|
is GlobalFeedState.Content -> {
|
||||||
FeeListContent(
|
FeedListContent(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
|
|
@ -125,7 +122,7 @@ private fun FeedSearchBar(feedListSearchBar: FeedListSearchBar, modifier: Modifi
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FeeListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
||||||
val background = LocalMainBottomSheetColor.current.value
|
val background = LocalMainBottomSheetColor.current.value
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
@ -289,6 +286,13 @@ private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendi
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val articlesReadStatus = remember(news.content) {
|
||||||
|
news.content.map { it.isViewed }
|
||||||
|
}
|
||||||
|
LaunchedEffect(articlesReadStatus) {
|
||||||
|
listState.requestScrollToItem(0)
|
||||||
|
}
|
||||||
Column {
|
Column {
|
||||||
Header(
|
Header(
|
||||||
title = {
|
title = {
|
||||||
|
|
@ -347,7 +351,7 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
state = rememberLazyListState(),
|
state = listState,
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = news.content,
|
items = news.content,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import com.tangem.core.ui.components.block.BlockCard
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun FeeListLoading(modifier: Modifier = Modifier) {
|
internal fun FeedListLoading(modifier: Modifier = Modifier) {
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -203,6 +205,13 @@ private fun LazyListScope.loadingInfoBlocks() {
|
||||||
|
|
||||||
private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
|
private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
|
||||||
item("related-news") {
|
item("related-news") {
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val articlesReadStatus = remember(relatedNews.articles) {
|
||||||
|
relatedNews.articles.map { it.isViewed }
|
||||||
|
}
|
||||||
|
LaunchedEffect(articlesReadStatus) {
|
||||||
|
listState.requestScrollToItem(0)
|
||||||
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -220,7 +229,7 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
state = rememberLazyListState(),
|
state = listState,
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = relatedNews.articles,
|
items = relatedNews.articles,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.ui.platform.testTag
|
||||||
import com.tangem.common.ui.markets.MarketsListItem
|
import com.tangem.common.ui.markets.MarketsListItem
|
||||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||||
import com.tangem.common.ui.markets.models.MarketsListItemUM.Companion.TOKEN_LAZY_LIST_ID_SEPARATOR
|
import com.tangem.common.ui.markets.models.MarketsListItemUM.Companion.TOKEN_LAZY_LIST_ID_SEPARATOR
|
||||||
|
import com.tangem.core.ui.components.SpacerH12
|
||||||
import com.tangem.core.ui.components.UnableToLoadData
|
import com.tangem.core.ui.components.UnableToLoadData
|
||||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||||
|
|
@ -176,6 +177,7 @@ private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Mod
|
||||||
onClick = onShowTokensClick,
|
onClick = onShowTokensClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
SpacerH12()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.features.feed.ui.news.details
|
package com.tangem.features.feed.ui.news.details
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
|
@ -20,6 +21,7 @@ import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
|
@ -40,12 +42,45 @@ import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
import com.tangem.features.feed.ui.news.details.components.NewsDetailsPlaceholder
|
||||||
import com.tangem.features.feed.ui.news.details.components.RelatedTokensBlock
|
import com.tangem.features.feed.ui.news.details.components.RelatedTokensBlock
|
||||||
import com.tangem.features.feed.ui.news.details.state.*
|
import com.tangem.features.feed.ui.news.details.state.*
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modifier) {
|
internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modifier) {
|
||||||
val background = LocalMainBottomSheetColor.current.value
|
val background = LocalMainBottomSheetColor.current.value
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = state.articlesStateUM,
|
||||||
|
modifier = modifier,
|
||||||
|
) { animatedState ->
|
||||||
|
when (animatedState) {
|
||||||
|
ArticlesStateUM.Content -> {
|
||||||
|
Content(state = state, background = background)
|
||||||
|
}
|
||||||
|
ArticlesStateUM.Loading -> {
|
||||||
|
NewsDetailsPlaceholder(background = background)
|
||||||
|
}
|
||||||
|
is ArticlesStateUM.LoadingError -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(background),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
UnableToLoadData(
|
||||||
|
onRetryClick = animatedState.onRetryClicked,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 35.dp, horizontal = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Content(state: NewsDetailsUM, background: Color) {
|
||||||
val pagerState = rememberPagerState(
|
val pagerState = rememberPagerState(
|
||||||
initialPage = state.selectedArticleIndex,
|
initialPage = state.selectedArticleIndex,
|
||||||
pageCount = { state.articles.size },
|
pageCount = { state.articles.size },
|
||||||
|
|
@ -67,7 +102,7 @@ internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modif
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(background),
|
.background(background),
|
||||||
) {
|
) {
|
||||||
|
|
@ -318,6 +353,7 @@ private fun PreviewNewsDetailsContent() {
|
||||||
TangemThemePreview {
|
TangemThemePreview {
|
||||||
NewsDetailsContent(
|
NewsDetailsContent(
|
||||||
state = NewsDetailsUM(
|
state = NewsDetailsUM(
|
||||||
|
articlesStateUM = ArticlesStateUM.Content,
|
||||||
articles = MockArticlesFactory.createMockArticles(),
|
articles = MockArticlesFactory.createMockArticles(),
|
||||||
selectedArticleIndex = 0,
|
selectedArticleIndex = 0,
|
||||||
onShareClick = {},
|
onShareClick = {},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package com.tangem.features.feed.ui.news.details.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.components.RectangleShimmer
|
||||||
|
import com.tangem.core.ui.components.SpacerH
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewsDetailsPlaceholder(background: Color, modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxSize().background(background).padding(16.dp),
|
||||||
|
) {
|
||||||
|
RectangleShimmer(modifier = Modifier.size(width = 112.dp, height = 20.dp))
|
||||||
|
SpacerH(8.dp)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(28.dp),
|
||||||
|
)
|
||||||
|
SpacerH(4.dp)
|
||||||
|
RectangleShimmer(modifier = Modifier.size(height = 28.dp, width = 208.dp))
|
||||||
|
SpacerH(20.dp)
|
||||||
|
RectangleShimmer(modifier = Modifier.size(height = 36.dp, width = 99.dp), radius = 12.dp)
|
||||||
|
SpacerH(32.dp)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(20.dp),
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp),
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp),
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 24.dp),
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 70.dp),
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp),
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 96.dp),
|
||||||
|
)
|
||||||
|
RectangleShimmer(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 100.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.models.news.RelatedToken
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
internal data class NewsDetailsUM(
|
internal data class NewsDetailsUM(
|
||||||
|
val articlesStateUM: ArticlesStateUM,
|
||||||
val articles: ImmutableList<ArticleUM>,
|
val articles: ImmutableList<ArticleUM>,
|
||||||
val selectedArticleIndex: Int,
|
val selectedArticleIndex: Int,
|
||||||
val onShareClick: () -> Unit,
|
val onShareClick: () -> Unit,
|
||||||
|
|
@ -17,6 +18,13 @@ internal data class NewsDetailsUM(
|
||||||
val relatedTokensUM: RelatedTokensUM = RelatedTokensUM.Loading,
|
val relatedTokensUM: RelatedTokensUM = RelatedTokensUM.Loading,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
internal sealed interface ArticlesStateUM {
|
||||||
|
data object Loading : ArticlesStateUM
|
||||||
|
data object Content : ArticlesStateUM
|
||||||
|
data class LoadingError(val onRetryClicked: () -> Unit) : ArticlesStateUM
|
||||||
|
}
|
||||||
|
|
||||||
internal data class ArticleUM(
|
internal data class ArticleUM(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue