Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-26 13:06:23 +01:00
parent a71b7ed4ba
commit 13f1db7739
16 changed files with 143 additions and 269 deletions

View file

@ -111,6 +111,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
router = innerRouter,
),
feedEntryClickIntents = clickIntents,
onBackClicked = { onChildBack() },
)
},
)

View file

@ -50,6 +50,7 @@ internal class FeedEntryChildFactory @Inject constructor(
child: Child,
appComponentContext: AppComponentContext,
feedEntryClickIntents: FeedEntryClickIntents,
onBackClicked: () -> Unit,
): ComposableModularBottomSheetContentComponent {
return when (child) {
is Child.TokenDetails -> {
@ -76,6 +77,10 @@ internal class FeedEntryChildFactory @Inject constructor(
Child.NewsList -> {
DefaultNewsListComponent(
appComponentContext = appComponentContext,
params = DefaultNewsListComponent.Params(
onArticleClicked = { feedEntryClickIntents.onArticleClick(articleId = it) },
onBackClick = onBackClicked,
),
)
}
Child.Feed -> {

View file

@ -2,20 +2,56 @@ package com.tangem.features.feed.components.news.list
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
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.R
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.features.feed.model.news.list.NewsListModel
import com.tangem.features.feed.ui.news.list.NewsListContent
import kotlinx.serialization.Serializable
internal class DefaultNewsListComponent(
appComponentContext: AppComponentContext,
private val params: Params,
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
private val newsListModel = getOrCreateModel<NewsListModel, Params>(params = params)
@Composable
override fun Title(bottomSheetState: State<BottomSheetState>) {
val background = LocalMainBottomSheetColor.current.value
val state by newsListModel.state.collectAsStateWithLifecycle()
TangemTopAppBar(
containerColor = background,
title = stringResourceSafe(R.string.common_news),
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_back_24,
onClicked = state.onBackClick,
isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED,
),
)
}
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
val state by newsListModel.state.collectAsStateWithLifecycle()
NewsListContent(
state = state,
modifier = modifier,
)
}
@Serializable
data class Params(
val onArticleClicked: (Int) -> Unit,
val onBackClick: () -> Unit,
)
}

View file

@ -6,6 +6,7 @@ import com.tangem.features.feed.model.feed.FeedComponentModel
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
import com.tangem.features.feed.model.market.list.MarketsListModel
import com.tangem.features.feed.model.news.details.NewsDetailsModel
import com.tangem.features.feed.model.news.list.NewsListModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -35,4 +36,9 @@ internal interface ModelModule {
@IntoMap
@ClassKey(NewsDetailsModel::class)
fun provideNewsDetailsModel(model: NewsDetailsModel): Model
@Binds
@IntoMap
@ClassKey(NewsListModel::class)
fun provideNewsListModel(model: NewsListModel): Model
}

View file

@ -0,0 +1,82 @@
package com.tangem.features.feed.model.news.list
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.chip.entity.ChipUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
import com.tangem.features.feed.ui.news.list.state.NewsListUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
internal class NewsListModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getNewsCategoriesUseCase: GetNewsCategoriesUseCase,
paramsContainer: ParamsContainer,
) : Model() {
private val params = paramsContainer.require<DefaultNewsListComponent.Params>()
private val _state = MutableStateFlow(
NewsListUM(
selectedCategoryId = 0,
filters = persistentListOf(),
articles = persistentListOf(),
onArticleClick = params.onArticleClicked,
onBackClick = params.onBackClick,
),
)
val state = _state.asStateFlow()
init {
modelScope.launch(dispatchers.default) {
val filterChips = getNewsCategoriesUseCase
.invoke()
.fold(
ifLeft = {
persistentListOf()
},
ifRight = { categories ->
categories.map { articleCategory ->
ChipUM(
id = articleCategory.id,
text = TextReference.Str(articleCategory.name),
isSelected = false,
onClick = {
onCategoryClick(articleCategory.id)
},
)
}.toImmutableList()
},
)
_state.update { currentState ->
currentState.copy(filters = filterChips)
}
}
}
private fun onCategoryClick(categoryId: Int) {
_state.update { currentState ->
currentState.copy(
selectedCategoryId = categoryId,
filters = updateFilterChips(categoryId),
)
}
}
private fun updateFilterChips(categoryId: Int): ImmutableList<ChipUM> {
return state.value.filters.map { chip ->
chip.copy(isSelected = chip.id == categoryId)
}.toImmutableList()
}
}

View file

@ -0,0 +1,146 @@
package com.tangem.features.feed.ui.news.list
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.news.ArticleCard
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.chip.Chip
import com.tangem.core.ui.components.chip.entity.ChipUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
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.news.list.state.NewsListUM
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableSet
@Composable
internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
Column(
modifier = modifier
.fillMaxSize()
.background(background),
) {
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(
items = state.filters,
key = { it.id },
) { filter ->
Chip(state = filter)
}
}
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(16.dp),
) {
items(
items = state.articles,
key = ArticleConfigUM::id,
) { article ->
ArticleCard(
articleConfigUM = article,
onArticleClick = { state.onArticleClick(article.id) },
modifier = Modifier
.fillMaxWidth()
.height(164.dp),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
)
SpacerH(12.dp)
}
}
}
}
@Suppress("LongMethod")
@Preview(showBackground = true)
@Composable
private fun NewsListContentPreview() {
val tags = listOf(
LabelUM(TextReference.Str("Regulation")),
LabelUM(TextReference.Str("BTC")),
LabelUM(TextReference.Str("ETH")),
).toImmutableSet()
val articles = persistentListOf(
ArticleConfigUM(
id = 1,
title = "SEC delays decisions on ETH-staking ETFs and spot XRP/SOL funds",
score = 6.5f,
createdAt = TextReference.Str("1h ago"),
isTrending = false,
tags = tags,
isViewed = false,
),
ArticleConfigUM(
id = 2,
title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)",
score = 8.6f,
createdAt = TextReference.Str("8h ago"),
isTrending = false,
tags = tags,
isViewed = true,
),
ArticleConfigUM(
id = 3,
title = "Bitcoin reclaims ~\$115K amid macro prints and ETF optimism",
score = 7.8f,
createdAt = TextReference.Str("22 Jun, 11:30"),
isTrending = false,
tags = tags,
isViewed = false,
),
)
val filters = persistentListOf(
ChipUM(
id = 0,
text = TextReference.Str("All News"),
isSelected = true,
onClick = {},
),
ChipUM(
id = 1,
text = TextReference.Str("Regulation"),
isSelected = false,
onClick = {},
),
ChipUM(
id = 2,
text = TextReference.Str("ETFs"),
isSelected = false,
onClick = {},
),
ChipUM(
id = 3,
text = TextReference.Str("Institutions"),
isSelected = false,
onClick = {},
),
)
TangemThemePreview {
NewsListContent(
state = NewsListUM(
selectedCategoryId = 0,
filters = filters,
articles = articles,
onArticleClick = {},
onBackClick = {},
),
)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.features.feed.ui.news.list.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.news.ArticleConfigUM
import com.tangem.core.ui.components.chip.entity.ChipUM
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class NewsListUM(
val selectedCategoryId: Int?,
val filters: ImmutableList<ChipUM>,
val articles: ImmutableList<ArticleConfigUM>,
val onArticleClick: (Int) -> Unit,
val onBackClick: () -> Unit,
)