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

@ -1,5 +1,6 @@
package com.tangem.domain.news.usecase
import arrow.core.Either
import com.tangem.domain.models.news.ArticleCategory
import com.tangem.domain.news.repository.NewsRepository
@ -15,7 +16,7 @@ class GetNewsCategoriesUseCase(
/**
* Fetches categories from the repository.
*/
suspend operator fun invoke(): List<ArticleCategory> {
return repository.getCategories()
suspend operator fun invoke(): Either<Throwable, List<ArticleCategory>> = Either.catch {
repository.getCategories()
}
}

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

@ -1,4 +1,4 @@
package com.tangem.features.news.list.impl.ui
package com.tangem.features.feed.ui.news.list
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -11,32 +11,27 @@ 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.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
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.extensions.stringResourceSafe
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, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
Column(
modifier = modifier
.fillMaxSize()
.background(color = TangemTheme.colors.background.tertiary),
.background(background),
) {
AppBarWithBackButton(
text = stringResourceSafe(R.string.common_news),
onBackClick = onBackClick,
)
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
@ -48,9 +43,8 @@ internal fun NewsListContent(state: NewsListUM, onBackClick: () -> Unit, modifie
Chip(state = filter)
}
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(16.dp),
) {
items(
@ -145,8 +139,8 @@ private fun NewsListContentPreview() {
filters = filters,
articles = articles,
onArticleClick = {},
onBackClick = {},
),
onBackClick = {},
)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.news.list.impl.ui
package com.tangem.features.feed.ui.news.list.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.news.ArticleConfigUM
@ -11,4 +11,5 @@ data class NewsListUM(
val filters: ImmutableList<ChipUM>,
val articles: ImmutableList<ArticleConfigUM>,
val onArticleClick: (Int) -> Unit,
val onBackClick: () -> Unit,
)

View file

@ -1 +0,0 @@
/build

View file

@ -1,20 +0,0 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.news.list.api"
}
dependencies {
implementation(deps.compose.foundation)
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Compose */
implementation(deps.compose.runtime)
}

View file

@ -1,23 +0,0 @@
package com.tangem.features.news.list.api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableContentComponent
interface NewsListComponent : ComposableContentComponent {
data class Params(val selectedFilter: String? = null)
@Composable
fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
)
interface Factory : ComponentFactory<Params, NewsListComponent>
}

View file

@ -1 +0,0 @@
/build

View file

@ -1,46 +0,0 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.news.list.impl"
}
dependencies {
/* AndroidX */
implementation(deps.lifecycle.compose)
implementation(deps.androidx.activity.compose)
/** Compose */
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.material3)
/** Core modules */
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.decompose)
implementation(projects.common.ui)
implementation(projects.common.routing)
/** Feature modules */
implementation(projects.features.news.newsList.api)
/** Domain modules */
implementation(projects.domain.models)
implementation(projects.domain.news)
/** Other dependencies */
implementation(deps.kotlin.immutable.collections)
implementation(deps.arrow.core)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -1,62 +0,0 @@
package com.tangem.features.news.list.impl
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.features.news.list.api.NewsListComponent
import com.tangem.features.news.list.impl.ui.NewsListContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNewsListComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: NewsListComponent.Params,
) : NewsListComponent, AppComponentContext by context {
private val model: NewsListModel = getOrCreateModel(params)
@Composable
override fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
val uiState by model.uiState.collectAsStateWithLifecycle()
val bsState by bottomSheetState
BackHandler(enabled = bsState == BottomSheetState.EXPANDED) {
navigateBack()
}
NewsListContent(
state = uiState,
onBackClick = ::navigateBack,
modifier = modifier,
)
}
@Composable
override fun Content(modifier: Modifier) {
val uiState by model.uiState.collectAsStateWithLifecycle()
NewsListContent(
state = uiState,
onBackClick = model::onBackClick,
modifier = modifier,
)
}
private fun navigateBack() = router.pop()
@AssistedFactory
interface Factory : NewsListComponent.Factory {
override fun create(context: AppComponentContext, params: NewsListComponent.Params): DefaultNewsListComponent
}
}

View file

@ -1,81 +0,0 @@
package com.tangem.features.news.list.impl
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
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.news.list.impl.ui.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.StateFlow
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,
private val router: Router,
) : Model() {
val uiState: StateFlow<NewsListUM>
field = MutableStateFlow(
NewsListUM(
selectedCategoryId = 0,
filters = persistentListOf(),
articles = persistentListOf(),
onArticleClick = ::onArticleClick,
),
)
init {
modelScope.launch(dispatchers.default) {
val filterChips = getNewsCategoriesUseCase
.invoke()
.map { articleCategory ->
ChipUM(
id = articleCategory.id,
text = TextReference.Str(articleCategory.name),
isSelected = false,
onClick = {
onCategoryClick(articleCategory.id)
},
)
}
.toImmutableList()
uiState.update { currentState ->
currentState.copy(filters = filterChips)
}
}
}
fun onBackClick() {
router.pop()
}
private fun onArticleClick(articleId: Int) {
// TODO [REDACTED_TASK_KEY]
articleId
}
private fun onCategoryClick(categoryId: Int) {
uiState.update { currentState ->
currentState.copy(
selectedCategoryId = categoryId,
filters = updateFilterChips(categoryId),
)
}
}
private fun updateFilterChips(categoryId: Int): ImmutableList<ChipUM> {
return uiState.value.filters.map { chip ->
chip.copy(isSelected = chip.id == categoryId)
}.toImmutableList()
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.features.news.list.impl.di
import com.tangem.features.news.list.api.NewsListComponent
import com.tangem.features.news.list.impl.DefaultNewsListComponent
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 NewsListModule {
@Binds
@Singleton
fun bindNewsListComponentFactory(factory: DefaultNewsListComponent.Factory): NewsListComponent.Factory
}