Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-12 10:01:22 +01:00
parent 9b134d7058
commit bc76eaa64a
46 changed files with 764 additions and 154 deletions

View file

@ -1,10 +1,13 @@
package com.tangem.features.feed.components
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
@ -13,11 +16,13 @@ import com.arkivanov.decompose.value.Value
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.entry.BottomSheetState
import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.feed.ui.EntryBottomSheetContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -35,7 +40,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
popCallback = { onChildBack() },
)
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, Any>> = childStack(
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, ComposableModularContentComponent>> = childStack(
key = "main",
source = stackNavigation,
serializer = FeedEntryChildFactory.Child.serializer(),
@ -59,7 +64,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
bottomSheetState // TODO will be continued in next tasks.
val stackState by stack.subscribeAsState()
BackHandler(enabled = bottomSheetState.value == BottomSheetState.EXPANDED) {
onChildBack()
}
EntryBottomSheetContent(
stackState = stackState,
onHeaderSizeChange = onHeaderSizeChange,
)
}
private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) {

View file

@ -3,6 +3,7 @@ package com.tangem.features.feed.components
import androidx.compose.runtime.Immutable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.feed.DefaultFeedComponent
@ -11,8 +12,9 @@ import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListCo
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
import kotlinx.serialization.Serializable
import javax.inject.Inject
internal class FeedEntryChildFactory {
internal class FeedEntryChildFactory @Inject constructor() {
@Serializable
@Immutable
@ -43,7 +45,7 @@ internal class FeedEntryChildFactory {
child: Child,
appComponentContext: AppComponentContext,
onTokenClick: (TokenMarketParams, AppCurrency) -> Unit,
): Any {
): ComposableModularContentComponent {
return when (child) {
is Child.TokenDetails -> {
DefaultMarketsTokenDetailsComponent(

View file

@ -1,23 +1,37 @@
package com.tangem.features.feed.components.feed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.features.feed.model.feed.FeedComponentModel
import com.tangem.features.feed.ui.feed.FeedListContent
import com.tangem.features.feed.ui.feed.FeedListHeader
internal class DefaultFeedComponent(
appComponentContext: AppComponentContext,
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
private val feedComponentModel = getOrCreateModel<FeedComponentModel>()
@Composable
override fun Title() {
val state by feedComponentModel.state.collectAsStateWithLifecycle()
FeedListHeader(state.searchBar)
}
@Composable
override fun Content(modifier: Modifier) {
val state by feedComponentModel.state.collectAsStateWithLifecycle()
FeedListContent(
modifier = modifier,
state = state,
)
}
@Composable
override fun Footer() {
}
override fun Footer() = Unit
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.feed.di
import com.tangem.features.feed.components.DefaultFeedEntryComponent
import com.tangem.features.feed.entry.components.FeedEntryComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ComponentModule {
@Binds
@Singleton
fun bindFeedEntryComponent(factory: DefaultFeedEntryComponent.Factory): FeedEntryComponent.Factory
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.feed.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.feed.model.feed.FeedComponentModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(FeedComponentModel::class)
fun bindsFeedComponentModel(model: FeedComponentModel): Model
}

View file

@ -0,0 +1,103 @@
package com.tangem.features.feed.model.feed
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.feed.state.*
import com.tangem.features.feed.ui.market.state.SortByTypeUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toPersistentHashMap
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import javax.inject.Inject
@Stable
@ModelScoped
internal class FeedComponentModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase,
private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase,
) : Model() {
private val _state = MutableStateFlow(initialState())
val state = _state.asStateFlow()
private val searchBarStateFactory by lazy(LazyThreadSafetyMode.NONE) {
SearchBarStateFactory(
currentStateProvider = Provider { _state.value },
onStateUpdate = { newState -> _state.update { newState } },
)
}
private val trendingNewsStateFactory by lazy(LazyThreadSafetyMode.NONE) {
TrendingNewsStateFactory(
currentStateProvider = Provider { _state.value },
onStateUpdate = { newState -> _state.update { newState } },
)
}
init {
modelScope.launch(dispatchers.default) {
fetchTrendingNewsUseCase()
subscribeOnTrendingNews()
}
_state.update { feedListUM ->
feedListUM.copy(
searchBar = _state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange),
)
}
}
private suspend fun subscribeOnTrendingNews() {
manageTrendingNewsUseCase().collect { articles ->
trendingNewsStateFactory.updateTrendingNewsState(articles)
}
}
private fun initialState(): FeedListUM {
return FeedListUM(
currentDate = getCurrentDate(),
searchBar = SearchBarUM(
placeholderText = resourceReference(R.string.markets_search_header_title),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = { },
),
feedListCallbacks = FeedListCallbacks(
onSearchClick = {},
onMarketOpenClick = {},
onArticleClick = {},
onOpenAllNews = {},
onMarketItemClick = {},
onSortTypeClick = {},
),
news = NewsUM.Loading,
trendingArticle = null,
marketChartConfig = MarketChartConfig(
marketCharts = buildMap {
SortByTypeUM.entries.forEach {
put(it, MarketChartUM.Loading)
} // TODO in [REDACTED_TASK_KEY] add correct sorting
}.toPersistentHashMap(),
currentSortByType = SortByTypeUM.TopGainers,
),
)
}
private fun getCurrentDate(): String {
val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault())
return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate)
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.features.feed.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.features.feed.components.FeedEntryChildFactory
@Composable
internal fun EntryBottomSheetContent(
stackState: ChildStack<FeedEntryChildFactory.Child, ComposableModularContentComponent>,
onHeaderSizeChange: (Dp) -> Unit,
) {
val density = LocalDensity.current
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
topBar = {
AnimatedContent(
targetState = stackState.active.instance,
modifier = Modifier.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {
with(density) {
onHeaderSizeChange(coordinates.size.height.toDp())
}
}
},
) { currentState ->
currentState.Title()
}
},
content = { contentPadding ->
AnimatedContent(
stackState.active.instance,
) { currentState ->
currentState.Content(modifier = Modifier.padding(contentPadding))
}
},
)
}

View file

@ -22,25 +22,25 @@ 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.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.common.ui.news.ArticleCard
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.common.ui.news.ArticleCard
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
@ -48,51 +48,44 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
import com.tangem.features.feed.ui.feed.state.FeedListCallbacks
import com.tangem.features.feed.ui.feed.state.FeedListUM
import com.tangem.features.feed.ui.feed.state.MarketChartConfig
import com.tangem.features.feed.ui.feed.state.MarketChartUM
import com.tangem.features.feed.ui.feed.state.*
import com.tangem.features.feed.ui.market.components.MarketsListItem
import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
import com.tangem.features.feed.ui.market.state.SortByTypeUM
import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
val density = LocalDensity.current
internal fun FeedListHeader(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
SearchBar(
modifier = modifier
.drawBehind { drawRect(background) }
.padding(horizontal = 16.dp)
.padding(bottom = 12.dp),
state = searchBarUM,
colors = TangemSearchBarDefaults.defaultTextFieldColors.copy(
focusedContainerColor = TangemTheme.colors.field.focused,
unfocusedContainerColor = TangemTheme.colors.field.focused,
),
)
}
@Composable
internal fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.drawBehind { drawRect(background) },
) {
SearchBar(
modifier = Modifier
.drawBehind { drawRect(background) }
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp,
)
.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {
with(density) {
onHeaderSizeChange(coordinates.size.height.toDp())
}
}
}
.padding(bottom = 4.dp),
state = state.searchBar,
)
SpacerH(20.dp)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.padding(horizontal = 20.dp),
text = stringResourceSafe(R.string.feed_market_and_news),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
@ -100,7 +93,7 @@ internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modif
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.padding(horizontal = 20.dp),
text = state.currentDate,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.tertiary,
@ -156,6 +149,17 @@ private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks:
@Composable
private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
if (marketChartConfig.marketCharts.isNotEmpty()) {
Header(
title = {
Text(
text = stringResourceSafe(R.string.markets_common_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
},
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) },
)
LazyRow(
modifier = Modifier.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
@ -175,17 +179,6 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb
}
}
Header(
title = {
Text(
text = stringResourceSafe(R.string.markets_common_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
},
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) },
)
SpacerH(12.dp)
AnimatedContent(
@ -207,18 +200,38 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb
@Suppress("CanBeNonNullable")
@Composable
private fun NewsBlock(
private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
AnimatedContent(news) { newsUM ->
when (newsUM) {
is NewsUM.Content -> {
if (newsUM.content.isNotEmpty()) {
NewsContentBlock(
feedListCallbacks = feedListCallbacks,
news = newsUM,
trendingArticle = trendingArticle,
)
}
}
NewsUM.Loading -> {
NewsLoadingBlock()
}
}
}
}
@Composable
private fun NewsContentBlock(
feedListCallbacks: FeedListCallbacks,
news: ImmutableList<ArticleConfigUM>,
news: NewsUM.Content,
trendingArticle: ArticleConfigUM?,
) {
if (news.isNotEmpty()) {
Column {
Header(
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResourceSafe(R.string.common_news),
style = TangemTheme.typography.subtitle1,
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
@ -244,13 +257,12 @@ private fun NewsBlock(
append(stringResourceSafe(R.string.feed_tangem_ai))
}
},
style = TangemTheme.typography.subtitle1,
style = TangemTheme.typography.h3,
)
}
},
onSeeAllClick = feedListCallbacks.onOpenAllNews,
)
SpacerH(12.dp)
trendingArticle?.let { article ->
@ -260,8 +272,8 @@ private fun NewsBlock(
.padding(horizontal = 16.dp),
articleConfigUM = article,
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
)
SpacerH(12.dp)
}
@ -272,13 +284,16 @@ private fun NewsBlock(
state = rememberLazyListState(),
) {
items(
items = news,
items = news.content,
key = ArticleConfigUM::id,
) { article ->
ArticleCard(
articleConfigUM = article,
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
modifier = Modifier.size(164.dp),
modifier = Modifier
.height(164.dp)
.widthIn(max = 216.dp),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
)
}
}
@ -292,6 +307,7 @@ private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) {
.fillMaxWidth()
.padding(horizontal = 20.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
title()
@ -310,7 +326,10 @@ private fun Charts(
onItemClick: (MarketsListItemUM) -> Unit,
modifier: Modifier = Modifier,
) {
BlockCard(modifier) {
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(
modifier = Modifier
.fillMaxWidth()
@ -380,9 +399,6 @@ private val LinearGradientSecondPart = Color(0xFFE05AED)
@Composable
private fun FeedListPreview() {
TangemThemePreview {
FeedList(
state = createFeedPreviewState(),
onHeaderSizeChange = {},
)
FeedListContent(state = createFeedPreviewState())
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.features.feed.ui.feed
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.news.DefaultLoadingArticle
import com.tangem.common.ui.news.TrendingLoadingArticle
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder
@Composable
internal fun MarketLoadingBlock() {
RectangleShimmer(
modifier = Modifier
.padding(start = 16.dp)
.size(width = 104.dp, height = 18.dp),
)
SpacerH(12.dp)
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(32.dp)
}
@Composable
internal fun MarketPulseLoadingBlock() {
RectangleShimmer()
SpacerH(8.dp)
LazyRow(
modifier = Modifier.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
state = rememberLazyListState(),
) {
items(DEFAULT_CHART_SIZE_IN_MARKET) {
RectangleShimmer(modifier = Modifier.size(width = 124.dp, height = 36.dp))
}
}
SpacerH(12.dp)
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(32.dp)
}
@Composable
internal fun NewsLoadingBlock() {
Column {
RectangleShimmer()
SpacerH(12.dp)
TrendingLoadingArticle(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(12.dp)
LazyRow(
verticalAlignment = Alignment.CenterVertically,
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
state = rememberLazyListState(),
) {
items(DEFAULT_CHART_SIZE_IN_MARKET) {
DefaultLoadingArticle()
}
}
}
}
@Composable
private fun ChartsLoading(modifier: Modifier = Modifier) {
BlockCard(modifier) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
repeat(DEFAULT_CHART_SIZE_IN_MARKET) {
MarketsListItemPlaceholder()
}
}
}
}
private const val DEFAULT_CHART_SIZE_IN_MARKET = 5
@Preview(showBackground = true)
@Composable
private fun FeedListLoadingPreview() {
TangemThemePreview {
Column {
NewsLoadingBlock()
SpacerH(10.dp)
MarketLoadingBlock()
SpacerH(10.dp)
MarketPulseLoadingBlock()
}
}
}

View file

@ -1,16 +1,15 @@
package com.tangem.features.feed.ui.feed.preview
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.feed.state.*
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
import com.tangem.features.feed.ui.market.state.SortByTypeUM
@ -39,7 +38,7 @@ internal object FeedListPreviewDataProvider {
onMarketItemClick = {},
onSortTypeClick = {},
),
news = articles.filter { it.isTrending.not() }.toImmutableList(),
news = NewsUM.Content(articles.filter { it.isTrending.not() }.toImmutableList()),
trendingArticle = articles.first { it.isTrending },
marketChartConfig = MarketChartConfig(
marketCharts = createMarketCharts(marketItems, includeErrorState = false),
@ -165,7 +164,7 @@ internal object FeedListPreviewDataProvider {
LabelUM(
text = TextReference.Str("BTC"),
leadingContent = LabelLeadingContentUM.Token(
icon = R.drawable.img_btc_22,
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/euro-coin.png",
),
),
LabelUM(TextReference.Str("Regulation")),

View file

@ -14,7 +14,7 @@ internal data class FeedListUM(
val currentDate: String,
val searchBar: SearchBarUM,
val feedListCallbacks: FeedListCallbacks,
val news: ImmutableList<ArticleConfigUM>,
val news: NewsUM,
val trendingArticle: ArticleConfigUM?,
val marketChartConfig: MarketChartConfig,
)
@ -28,6 +28,12 @@ internal data class FeedListCallbacks(
val onSortTypeClick: (SortByTypeUM) -> Unit,
)
@Immutable
internal sealed interface NewsUM {
data object Loading : NewsUM
data class Content(val content: ImmutableList<ArticleConfigUM>) : NewsUM
}
internal data class MarketChartConfig(
val marketCharts: ImmutableMap<SortByTypeUM, MarketChartUM>,
val currentSortByType: SortByTypeUM = SortByTypeUM.TopGainers,
@ -49,7 +55,7 @@ internal sealed interface MarketChartUM {
data class LoadingError(val onRetryClicked: () -> Unit) : MarketChartUM
}
data class SortChartConfigUM(
internal data class SortChartConfigUM(
val sortByType: SortByTypeUM,
val isSelected: Boolean,
)

View file

@ -0,0 +1,24 @@
package com.tangem.features.feed.ui.feed.state
import com.tangem.utils.Provider
internal class SearchBarStateFactory(
private val currentStateProvider: Provider<FeedListUM>,
private val onStateUpdate: (FeedListUM) -> Unit,
) {
val searchQuery: String
get() = currentStateProvider().searchBar.query
fun onSearchQueryChange(query: String) {
val currentState = currentStateProvider()
onStateUpdate(
currentState.copy(
searchBar = currentState.searchBar.copy(
query = query,
isActive = query.isNotEmpty(),
),
),
)
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.features.feed.ui.feed.state
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.news.ShortArticle
import com.tangem.utils.Provider
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
internal class TrendingNewsStateFactory(
private val currentStateProvider: Provider<FeedListUM>,
private val onStateUpdate: (FeedListUM) -> Unit,
) {
fun updateTrendingNewsState(news: List<ShortArticle>) {
val trendingArticleIndex = news.indexOfFirst { it.isTrending }
val trendingArticle = if (trendingArticleIndex != -1) news[trendingArticleIndex] else null
val commonArticles = if (trendingArticleIndex != -1) {
news.toMutableList().apply { removeAt(trendingArticleIndex) }
} else {
news
}
val currentState = currentStateProvider()
onStateUpdate(
currentState.copy(
trendingArticle = trendingArticle?.let { article ->
ArticleConfigUM(
id = article.id,
title = article.title,
score = article.score,
isTrending = true,
tags = article.categories.map { category ->
LabelUM(text = TextReference.Str(category.name))
}.plus(
article.relatedTokens.map { token ->
LabelUM(
text = TextReference.Str(token.symbol),
leadingContent = LabelLeadingContentUM.Token(
iconUrl = getTokenIconUrlFromDefaultHost(
tokenId = CryptoCurrency.RawID(token.id),
),
),
)
},
).toPersistentSet(),
createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY]
isViewed = article.viewed,
)
},
news = NewsUM.Content(
commonArticles.map { article ->
ArticleConfigUM(
id = article.id,
title = article.title,
score = article.score,
isTrending = false,
tags = article.categories.map { category ->
LabelUM(text = TextReference.Str(category.name))
}.plus(
article.relatedTokens.map { token ->
LabelUM(
text = TextReference.Str(token.symbol),
leadingContent = LabelLeadingContentUM.Token(
iconUrl = getTokenIconUrlFromDefaultHost(
tokenId = CryptoCurrency.RawID(token.id),
),
),
)
},
).toPersistentSet(),
createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY]
isViewed = article.viewed,
)
}.toPersistentList(),
),
),
)
}
}