Updated on 2026-08-14
This commit is contained in:
parent
a71b7ed4ba
commit
13f1db7739
16 changed files with 143 additions and 269 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -111,6 +111,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
router = innerRouter,
|
||||
),
|
||||
feedEntryClickIntents = clickIntents,
|
||||
onBackClicked = { onChildBack() },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
1
features/news/news-list/api/.gitignore
vendored
1
features/news/news-list/api/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/build
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
1
features/news/news-list/impl/.gitignore
vendored
1
features/news/news-list/impl/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/build
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue