From 46a4a937113f7968fe31f32e888cf3b6ecdac5d2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Dec 2025 13:19:26 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tangem/core/ui/components/chip/Chip.kt | 122 ++++++++++++++ .../core/ui/components/chip/entity/ChipUM.kt | 10 ++ .../tangem/core/ui/components/label/Label.kt | 4 +- .../news/news-details/api/build.gradle.kts | 2 + .../news/details/api/NewsDetailsComponent.kt | 12 ++ .../impl/DefaultNewsDetailsComponent.kt | 32 +++- .../details/impl/ui/NewsDetailsContent.kt | 84 +++++----- features/news/news-list/api/.gitignore | 1 + features/news/news-list/api/build.gradle.kts | 20 +++ .../news/list/api/NewsListComponent.kt | 23 +++ features/news/news-list/impl/.gitignore | 1 + features/news/news-list/impl/build.gradle.kts | 46 ++++++ .../list/impl/DefaultNewsListComponent.kt | 62 +++++++ .../features/news/list/impl/NewsListModel.kt | 81 ++++++++++ .../news/list/impl/di/NewsListModule.kt | 18 +++ .../news/list/impl/ui/NewsListContent.kt | 152 ++++++++++++++++++ .../features/news/list/impl/ui/NewsListUM.kt | 14 ++ features/tester/impl/build.gradle.kts | 2 + .../tester/presentation/TesterActivity.kt | 42 +++++ .../presentation/navigation/TesterScreen.kt | 1 + .../tester/presentation/news/state/NewsUM.kt | 1 + .../news/viewmodel/NewsViewModel.kt | 2 + .../impl/src/main/res/values/strings.xml | 1 + settings.gradle.kts | 3 + 24 files changed, 693 insertions(+), 43 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/chip/entity/ChipUM.kt create mode 100644 features/news/news-list/api/.gitignore create mode 100644 features/news/news-list/api/build.gradle.kts create mode 100644 features/news/news-list/api/src/main/kotlin/com/tangem/features/news/list/api/NewsListComponent.kt create mode 100644 features/news/news-list/impl/.gitignore create mode 100644 features/news/news-list/impl/build.gradle.kts create mode 100644 features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/DefaultNewsListComponent.kt create mode 100644 features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/NewsListModel.kt create mode 100644 features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/di/NewsListModule.kt create mode 100644 features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/ui/NewsListContent.kt create mode 100644 features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/ui/NewsListUM.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt b/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt new file mode 100644 index 0000000000..fb14a4a72e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt @@ -0,0 +1,122 @@ +package com.tangem.core.ui.components.chip + +import android.content.res.Configuration +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.chip.entity.ChipUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun Chip(state: ChipUM, modifier: Modifier = Modifier) { + val backgroundColor by animateColorAsState( + targetValue = if (state.isSelected) { + TangemTheme.colors.button.primary + } else { + TangemTheme.colors.button.secondary + }, + ) + + val textColor by animateColorAsState( + targetValue = if (state.isSelected) { + TangemTheme.colors.text.primary2 + } else { + TangemTheme.colors.text.primary1 + }, + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(color = backgroundColor) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + onClick = state.onClick, + ) + .padding(PaddingValues(horizontal = 24.dp, vertical = 8.dp)), + ) { + Text( + text = state.text.resolveReference(), + style = TangemTheme.typography.button, + color = textColor, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ChipPreview() { + TangemThemePreview { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .padding(16.dp), + ) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Chip( + state = ChipUM( + id = 0, + text = TextReference.Str("All News"), + isSelected = true, + onClick = {}, + ), + ) + Chip( + state = ChipUM( + id = 1, + text = TextReference.Str("Regulation"), + isSelected = false, + onClick = {}, + ), + ) + Chip( + state = ChipUM( + id = 2, + text = TextReference.Str("ETFs"), + isSelected = false, + onClick = {}, + ), + ) + Chip( + state = ChipUM( + id = 3, + text = TextReference.Str("Institutions"), + isSelected = false, + onClick = {}, + ), + ) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/chip/entity/ChipUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/chip/entity/ChipUM.kt new file mode 100644 index 0000000000..a2785cb42b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/chip/entity/ChipUM.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.components.chip.entity + +import com.tangem.core.ui.extensions.TextReference + +data class ChipUM( + val id: Int, + val text: TextReference, + val isSelected: Boolean = false, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt index 0f6b07e0d1..4b3d1db8d6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt @@ -168,7 +168,9 @@ private fun LabelPreview() { TangemThemePreview { Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(16.dp), + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .padding(16.dp), ) { FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), diff --git a/features/news/news-details/api/build.gradle.kts b/features/news/news-details/api/build.gradle.kts index b68f6815c6..a9c29a2049 100644 --- a/features/news/news-details/api/build.gradle.kts +++ b/features/news/news-details/api/build.gradle.kts @@ -9,6 +9,8 @@ android { } dependencies { + implementation(deps.compose.foundation) + /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) diff --git a/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt b/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt index 167f65b88f..56d379faf0 100644 --- a/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt +++ b/features/news/news-details/api/src/main/kotlin/com/tangem/features/news/details/api/NewsDetailsComponent.kt @@ -1,11 +1,23 @@ package com.tangem.features.news.details.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 NewsDetailsComponent : ComposableContentComponent { data class Params(val selectedArticleId: Int = 0) + @Composable + fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt index d68562beb6..98c8989e54 100644 --- a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/DefaultNewsDetailsComponent.kt @@ -1,11 +1,15 @@ package com.tangem.features.news.details.impl +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.getValue -import androidx.compose.runtime.collectAsState 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.details.api.NewsDetailsComponent import com.tangem.features.news.details.impl.ui.NewsDetailsContent import dagger.assisted.Assisted @@ -19,16 +23,40 @@ internal class DefaultNewsDetailsComponent @AssistedInject constructor( private val model: NewsDetailsModel = getOrCreateModel(params) + @Composable + override fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) { + val uiState by model.uiState.collectAsStateWithLifecycle() + val bsState by bottomSheetState + + BackHandler(enabled = bsState == BottomSheetState.EXPANDED) { + navigateBack() + } + + NewsDetailsContent( + state = uiState, + onBackClick = ::navigateBack, + modifier = modifier, + isBottomSheetMode = true, + ) + } + @Composable override fun Content(modifier: Modifier) { - val uiState by model.uiState.collectAsState() + val uiState by model.uiState.collectAsStateWithLifecycle() NewsDetailsContent( state = uiState, onBackClick = model::onBackClick, modifier = modifier, + isBottomSheetMode = false, ) } + private fun navigateBack() = router.pop() + @AssistedFactory interface Factory : NewsDetailsComponent.Factory { override fun create( diff --git a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt index a149f57bf4..a2c5e93a36 100644 --- a/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt +++ b/features/news/news-details/impl/src/main/kotlin/com/tangem/features/news/details/impl/ui/NewsDetailsContent.kt @@ -24,13 +24,19 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.components.pager.PagerIndicator +import com.tangem.core.ui.extensions.conditionalCompose import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.news.details.impl.MockArticlesFactory // TODO [REDACTED_TASK_KEY] make internal @Composable -fun NewsDetailsContent(state: NewsDetailsUM, onBackClick: () -> Unit, modifier: Modifier = Modifier) { +fun NewsDetailsContent( + state: NewsDetailsUM, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + isBottomSheetMode: Boolean = false, +) { val pagerState = rememberPagerState( initialPage = state.selectedArticleIndex, pageCount = { state.articles.size }, @@ -39,49 +45,47 @@ fun NewsDetailsContent(state: NewsDetailsUM, onBackClick: () -> Unit, modifier: Column( modifier = modifier .fillMaxSize() - .background(TangemTheme.colors.background.secondary) - .systemBarsPadding(), + .background(TangemTheme.colors.background.tertiary) + .conditionalCompose(isBottomSheetMode) { systemBarsPadding() }, ) { - Column { - TangemTopAppBar( - title = null, - startButton = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_back_24, - onClicked = onBackClick, - ), - endButton = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_share_24, - onClicked = state.onShareClick, - ), - ) - Box( - modifier = Modifier.fillMaxSize(), - ) { - if (state.articles.isNotEmpty()) { - HorizontalPager( - state = pagerState, + TangemTopAppBar( + title = null, + startButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_back_24, + onClicked = onBackClick, + ), + endButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_share_24, + onClicked = state.onShareClick, + ), + ) + Box( + modifier = Modifier.fillMaxSize(), + ) { + if (state.articles.isNotEmpty()) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + ) { page -> + ArticleDetail( + article = state.articles[page], modifier = Modifier.fillMaxSize(), - ) { page -> - ArticleDetail( - article = state.articles[page], - modifier = Modifier.fillMaxSize(), - onLikeClick = state.onLikeClick, - ) - } + onLikeClick = state.onLikeClick, + ) + } - if (state.articles.size > 1) { - Column( + if (state.articles.size > 1) { + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth(), + ) { + PagerIndicator( + pagerState = pagerState, modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth(), - ) { - PagerIndicator( - pagerState = pagerState, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(bottom = 16.dp), - ) - } + .align(Alignment.CenterHorizontally) + .padding(bottom = 16.dp), + ) } } } diff --git a/features/news/news-list/api/.gitignore b/features/news/news-list/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/news/news-list/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/news/news-list/api/build.gradle.kts b/features/news/news-list/api/build.gradle.kts new file mode 100644 index 0000000000..2de4c641e7 --- /dev/null +++ b/features/news/news-list/api/build.gradle.kts @@ -0,0 +1,20 @@ +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) +} \ No newline at end of file diff --git a/features/news/news-list/api/src/main/kotlin/com/tangem/features/news/list/api/NewsListComponent.kt b/features/news/news-list/api/src/main/kotlin/com/tangem/features/news/list/api/NewsListComponent.kt new file mode 100644 index 0000000000..225a75a329 --- /dev/null +++ b/features/news/news-list/api/src/main/kotlin/com/tangem/features/news/list/api/NewsListComponent.kt @@ -0,0 +1,23 @@ +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, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/news/news-list/impl/.gitignore b/features/news/news-list/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/news/news-list/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/news/news-list/impl/build.gradle.kts b/features/news/news-list/impl/build.gradle.kts new file mode 100644 index 0000000000..baf05ca033 --- /dev/null +++ b/features/news/news-list/impl/build.gradle.kts @@ -0,0 +1,46 @@ +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) +} \ No newline at end of file diff --git a/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/DefaultNewsListComponent.kt b/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/DefaultNewsListComponent.kt new file mode 100644 index 0000000000..1ed37bdc4e --- /dev/null +++ b/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/DefaultNewsListComponent.kt @@ -0,0 +1,62 @@ +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, + 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 + } +} \ No newline at end of file diff --git a/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/NewsListModel.kt b/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/NewsListModel.kt new file mode 100644 index 0000000000..18c010aaac --- /dev/null +++ b/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/NewsListModel.kt @@ -0,0 +1,81 @@ +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 + 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 { + return uiState.value.filters.map { chip -> + chip.copy(isSelected = chip.id == categoryId) + }.toImmutableList() + } +} \ No newline at end of file diff --git a/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/di/NewsListModule.kt b/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/di/NewsListModule.kt new file mode 100644 index 0000000000..b3d323d053 --- /dev/null +++ b/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/di/NewsListModule.kt @@ -0,0 +1,18 @@ +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 +} \ No newline at end of file diff --git a/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/ui/NewsListContent.kt b/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/ui/NewsListContent.kt new file mode 100644 index 0000000000..ea8a333901 --- /dev/null +++ b/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/ui/NewsListContent.kt @@ -0,0 +1,152 @@ +package com.tangem.features.news.list.impl.ui + +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.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.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableSet + +@Composable +internal fun NewsListContent(state: NewsListUM, onBackClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .background(color = TangemTheme.colors.background.tertiary), + ) { + AppBarWithBackButton( + text = stringResourceSafe(R.string.common_news), + onBackClick = onBackClick, + ) + + 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.fillMaxSize(), + 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 = {}, + ) + } +} \ No newline at end of file diff --git a/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/ui/NewsListUM.kt b/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/ui/NewsListUM.kt new file mode 100644 index 0000000000..d3e853c285 --- /dev/null +++ b/features/news/news-list/impl/src/main/kotlin/com/tangem/features/news/list/impl/ui/NewsListUM.kt @@ -0,0 +1,14 @@ +package com.tangem.features.news.list.impl.ui + +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, + val articles: ImmutableList, + val onArticleClick: (Int) -> Unit, +) \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index ba40b749c4..92a8671ee4 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -61,6 +61,8 @@ dependencies { implementation(projects.features.pushNotifications.api) implementation(projects.features.news.newsDetails.api) implementation(projects.features.news.newsDetails.impl) + implementation(projects.features.news.newsList.api) + implementation(projects.features.news.newsList.impl) /* SDK */ implementation(tangemDeps.blockchain) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 393fac33ad..333c9fd9e6 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -14,6 +14,9 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeActivity import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen @@ -38,9 +41,11 @@ import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewMod import com.tangem.feature.tester.presentation.news.ui.NewsScreen import com.tangem.feature.tester.presentation.news.viewmodel.NewsViewModel import com.tangem.features.news.details.impl.MockArticlesFactory +import com.tangem.features.news.details.impl.ui.ArticleUM import com.tangem.features.news.details.impl.ui.NewsDetailsContent import com.tangem.features.news.details.impl.ui.NewsDetailsUM import dagger.hilt.android.AndroidEntryPoint +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentSetOf import javax.inject.Inject @@ -190,6 +195,43 @@ internal class TesterActivity : ComposeActivity() { onBackClick = { innerTesterRouter.back() }, ) } + + composable(route = TesterScreen.NEWS_DETAILS_BOTTOM_SHEET.name) { + NewsDetailsBottomSheetTest( + onDismiss = { innerTesterRouter.back() }, + ) + } + } + } + + @Composable + private fun NewsDetailsBottomSheetTest(onDismiss: () -> Unit) { + data class NewsDetailsBottomSheetContent( + val articles: ImmutableList, + ) : TangemBottomSheetConfigContent + + val content = NewsDetailsBottomSheetContent( + articles = MockArticlesFactory.createMockArticles(), + ) + + TangemBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onDismiss, + content = content, + ), + containerColor = TangemTheme.colors.background.tertiary, + ) { sheetContent -> + NewsDetailsContent( + state = NewsDetailsUM( + articles = sheetContent.articles, + selectedArticleIndex = 0, + onLikeClick = { }, + onShareClick = { }, + ), + onBackClick = onDismiss, + isBottomSheetMode = true, + ) } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index 854706cec2..8d06310e39 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -16,4 +16,5 @@ internal enum class TesterScreen { ACCOUNTS, NEWS, NEWS_DETAILS, + NEWS_DETAILS_BOTTOM_SHEET, } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt index b832b363d8..43c8c9efca 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/state/NewsUM.kt @@ -12,5 +12,6 @@ data class NewsUM( enum class ButtonUM(@StringRes val textResId: Int) { NEWS_DETAILS(R.string.news_details), + NEWS_DETAILS_BOTTOM_SHEET(R.string.news_details_bottom_sheet), } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt index 5ed86201e4..f39c639ff5 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/news/viewmodel/NewsViewModel.kt @@ -27,6 +27,7 @@ internal class NewsViewModel @Inject constructor() : ViewModel() { onBackClick = ::onBackClick, buttons = persistentSetOf( NewsUM.ButtonUM.NEWS_DETAILS, + NewsUM.ButtonUM.NEWS_DETAILS_BOTTOM_SHEET, ), onButtonClick = ::onButtonClick, ) @@ -39,6 +40,7 @@ internal class NewsViewModel @Inject constructor() : ViewModel() { private fun onButtonClick(button: NewsUM.ButtonUM) { when (button) { NewsUM.ButtonUM.NEWS_DETAILS -> router?.open(TesterScreen.NEWS_DETAILS) + NewsUM.ButtonUM.NEWS_DETAILS_BOTTOM_SHEET -> router?.open(TesterScreen.NEWS_DETAILS_BOTTOM_SHEET) } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 15b31a07c6..901f76c14e 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -20,4 +20,5 @@ Accounts News News details + News details (Bottom Sheet) diff --git a/settings.gradle.kts b/settings.gradle.kts index 50802ec815..53bbc68e17 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -305,6 +305,9 @@ include(":features:feed:impl") include(":features:news:news-details:api") include(":features:news:news-details:impl") + +include(":features:news:news-list:api") +include(":features:news:news-list:impl") // endregion Feature modules // region Domain modules