From 970312afd3fa1ca555f6a4eb493196ae934b9757 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Apr 2026 18:02:28 +0500 Subject: [PATCH] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 10 +++ .../tangem/tap/routing/utils/ChildFactory.kt | 9 +++ .../tap/routing/utils/DeepLinkFactory.kt | 3 + .../tap/routing/utils/DeepLinkFactoryTest.kt | 21 +++++ .../com/tangem/common/routing/AppRoute.kt | 5 ++ .../tangem/common/routing/DeepLinkRoute.kt | 4 + .../common/routing/deeplink/DeeplinkConst.kt | 2 + .../routing/deeplink/DeepLinkBuilderTest.kt | 20 +++++ .../feed/entry/components/FeedEntryRoute.kt | 3 + .../entry/deeplink/NewsDeepLinkHandler.kt | 8 ++ .../components/DefaultFeedEntryComponent.kt | 5 +- .../feed/components/FeedEntryChildFactory.kt | 5 +- .../news/list/DefaultNewsListComponent.kt | 1 + .../deeplink/DefaultNewsDeepLinkHandler.kt | 35 ++++++++ .../feed/deeplink/di/FeedDeepLinkModule.kt | 6 ++ .../feed/model/news/list/NewsListModel.kt | 31 +++++-- .../feed/ui/news/list/NewsListContent.kt | 17 ++++ .../feed/ui/news/list/state/NewsListUM.kt | 5 +- .../DefaultNewsDeepLinkHandlerTest.kt | 80 +++++++++++++++++++ 19 files changed, 260 insertions(+), 10 deletions(-) create mode 100644 features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/NewsDeepLinkHandler.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDeepLinkHandler.kt create mode 100644 features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDeepLinkHandlerTest.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 95ce7f4c75..27f8c3c8e9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -296,6 +296,16 @@ android:host="onboard-visa" android:scheme="tangem" /> + + + + + + + + diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index a4d6cdc3b3..7351bc519d 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -700,6 +700,15 @@ internal class ChildFactory @Inject constructor( componentFactory = feedEntryComponentFactory, ) } + is AppRoute.News -> { + createComponentChild( + context = context, + params = FeedEntryRoute.NewsList( + preselectedCategoryId = route.categoryId, + ), + componentFactory = feedEntryComponentFactory, + ) + } } } } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 492d1d23ae..3e9d268dd5 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -9,6 +9,7 @@ import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsTokenExchangesDeepLinkHandler +import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler @@ -54,6 +55,7 @@ internal class DeepLinkFactory @Inject constructor( private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory, private val marketsTokenExchangesDeepLink: MarketsTokenExchangesDeepLinkHandler.Factory, private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory, + private val newsDeepLink: NewsDeepLinkHandler.Factory, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -159,6 +161,7 @@ internal class DeepLinkFactory @Inject constructor( DeepLinkRoute.WalletConnect.host -> walletConnectDeepLink.create(deeplinkUri) DeepLinkRoute.Promo.host -> promoDeepLink.create(coroutineScope, queryParams) DeepLinkRoute.OnboardVisa.host -> onboardVisaDeepLink.create(deeplinkUri) + DeepLinkRoute.News.host -> newsDeepLink.create(queryParams) else -> { TangemLogger.i( """ diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 43ba7a2555..7a1c5173b5 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -7,6 +7,7 @@ import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.feed.entry.deeplink.MarketsTokenExchangesDeepLinkHandler +import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler @@ -87,6 +88,10 @@ class DeepLinkFactoryTest { every { create(any(), any()) } returns mockk() } + private val newsDeepLinkFactory = mockk(relaxed = true) { + every { create(any()) } returns mockk() + } + private val marketsTokenExchangesDeepLinkFactory = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() @@ -116,6 +121,7 @@ class DeepLinkFactoryTest { promoDeepLink = promoDeepLinkFactory, onboardVisaDeepLink = onboardVisaDeepLink, newsDetailsDeepLink = newsDeeplink, + newsDeepLink = newsDeepLinkFactory, ) @OptIn(ExperimentalCoroutinesApi::class) @@ -432,6 +438,21 @@ class DeepLinkFactoryTest { verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) } } + @Test + fun `handleTangemDeepLinks routes news host to dedicated handler`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "news" + every { mockedUri.query } returns null + every { mockedUri.queryParameterNames } returns emptySet() + every { mockedUri.getQueryParameter(any()) } returns null + + deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) + advanceUntilIdle() + + verify { newsDeepLinkFactory.create(eq(emptyMap())) } + } + @Test fun `handleTangemDeepLinks routes to promo handler`() = runTest { every { mockedUri.scheme } returns "tangem" diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index b8c2d6bd0a..3a6eac7c41 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -489,4 +489,9 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class NewsDetails(val newsId: Int) : AppRoute(path = "/news_details/$newsId") + + @Serializable + data class News( + val categoryId: Int? = null, + ) : AppRoute(path = "/news") } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt index 359839b059..b9caabe5d0 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -71,6 +71,10 @@ sealed class DeepLinkRoute { data object PayApp : DeepLinkRoute() { override val host: String = "tangem.com" } + + data object News : DeepLinkRoute() { + override val host: String = "news" + } } enum class DeepLinkScheme(val scheme: String) { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt index 6fe8031620..8f745a3b32 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt @@ -18,4 +18,6 @@ object DeeplinkConst { const val ORDER_KEY = "order" const val INTERVAL_KEY = "interval" const val SECTION_KEY = "section" + const val CATEGORY_ID_KEY = "category_id" + const val NEWS_ID_KEY = "news_id" } \ No newline at end of file diff --git a/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt index 596d7cd6c2..bdcb99e48d 100644 --- a/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt +++ b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt @@ -88,6 +88,26 @@ internal class DeepLinkBuilderTest { assertThat(result).isEqualTo("${DeeplinkConst.TANGEM_SCHEME}://$action?$key1=$value1&$key2=$value2") } + @Test + fun `news host with categoryId produces expected uri`() { + val result = deepLinkBuilder + .setAction("news") + .addQueryParam(DeeplinkConst.CATEGORY_ID_KEY, "5") + .build() + + assertThat(result).isEqualTo("${DeeplinkConst.TANGEM_SCHEME}://news?${DeeplinkConst.CATEGORY_ID_KEY}=5") + } + + @Test + fun `news host with newsId produces expected uri`() { + val result = deepLinkBuilder + .setAction("news") + .addQueryParam(DeeplinkConst.NEWS_ID_KEY, "20533") + .build() + + assertThat(result).isEqualTo("${DeeplinkConst.TANGEM_SCHEME}://news?${DeeplinkConst.NEWS_ID_KEY}=20533") + } + @Test fun `GIVEN complex deep link WHEN build THEN should construct correct URI`() { // GIVEN diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt index ab97474343..098b29f4da 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryRoute.kt @@ -36,4 +36,7 @@ sealed interface FeedEntryRoute { @Serializable data class NewsDetail(val articleId: Int, val preselectedArticlesId: List) : FeedEntryRoute + + @Serializable + data class NewsList(val preselectedCategoryId: Int? = null) : FeedEntryRoute } \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/NewsDeepLinkHandler.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/NewsDeepLinkHandler.kt new file mode 100644 index 0000000000..4beed492b5 --- /dev/null +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/deeplink/NewsDeepLinkHandler.kt @@ -0,0 +1,8 @@ +package com.tangem.features.feed.entry.deeplink + +interface NewsDeepLinkHandler { + + interface Factory { + fun create(queryParams: Map): NewsDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt index a0b3a03088..198cdac711 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -125,7 +125,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( } override fun onOpenAllNews() { - innerRouter.push(FeedEntryChildFactory.Child.NewsList) + innerRouter.push(FeedEntryChildFactory.Child.NewsList()) } override fun onOpenEarnPage() { @@ -263,6 +263,9 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( }, ), ) + is FeedEntryRoute.NewsList -> FeedEntryChildFactory.Child.NewsList( + preselectedCategoryId = entryRoute.preselectedCategoryId, + ) null -> FeedEntryChildFactory.Child.Feed } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index 768445bbd6..fb8c3a7f90 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -53,7 +53,7 @@ internal class FeedEntryChildFactory @Inject constructor( @Serializable @Immutable - data object NewsList : Child + data class NewsList(val preselectedCategoryId: Int? = null) : Child @Serializable @Immutable @@ -110,7 +110,7 @@ internal class FeedEntryChildFactory @Inject constructor( params = child.params, ) } - Child.NewsList -> { + is Child.NewsList -> { DefaultNewsListComponent( appComponentContext = appComponentContext, params = Params( @@ -123,6 +123,7 @@ internal class FeedEntryChildFactory @Inject constructor( ) }, onBackClick = onBackClicked, + preselectedCategoryId = child.preselectedCategoryId, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/list/DefaultNewsListComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/list/DefaultNewsListComponent.kt index b93cac2f9f..1325a29be6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/list/DefaultNewsListComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/list/DefaultNewsListComponent.kt @@ -112,5 +112,6 @@ internal class DefaultNewsListComponent( paginationConfig: NewsListConfig?, ) -> Unit, val onBackClick: () -> Unit, + val preselectedCategoryId: Int? = null, ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDeepLinkHandler.kt new file mode 100644 index 0000000000..66aab9b849 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDeepLinkHandler.kt @@ -0,0 +1,35 @@ +package com.tangem.features.feed.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.deeplink.DeeplinkConst.CATEGORY_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.NEWS_ID_KEY +import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultNewsDeepLinkHandler @AssistedInject constructor( + @Assisted private val queryParams: Map, + private val appRouter: AppRouter, +) : NewsDeepLinkHandler { + + init { + handleDeepLink() + } + + private fun handleDeepLink() { + val newsId = queryParams[NEWS_ID_KEY]?.toIntOrNull() + if (newsId != null) { + appRouter.push(AppRoute.NewsDetails(newsId = newsId)) + return + } + + appRouter.push(AppRoute.News(categoryId = queryParams[CATEGORY_ID_KEY]?.toIntOrNull())) + } + + @AssistedFactory + interface Factory : NewsDeepLinkHandler.Factory { + override fun create(queryParams: Map): DefaultNewsDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt index d00b30cc79..05f42eb535 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/di/FeedDeepLinkModule.kt @@ -1,6 +1,8 @@ package com.tangem.features.feed.deeplink.di +import com.tangem.features.feed.deeplink.DefaultNewsDeepLinkHandler import com.tangem.features.feed.deeplink.DefaultNewsDetailsDeepLinkHandler +import com.tangem.features.feed.entry.deeplink.NewsDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler import dagger.Binds import dagger.Module @@ -17,4 +19,8 @@ internal interface FeedDeepLinkModule { fun bindNewsDetailsDeepLinkHandlerFactory( impl: DefaultNewsDetailsDeepLinkHandler.Factory, ): NewsDetailsDeepLinkHandler.Factory + + @Binds + @Singleton + fun bindNewsDeepLinkHandlerFactory(impl: DefaultNewsDeepLinkHandler.Factory): NewsDeepLinkHandler.Factory } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt index 409a942b93..483ad0271e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt @@ -6,6 +6,8 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.components.chip.entity.ChipUM +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.domain.news.model.NewsListConfig import com.tangem.domain.news.usecase.GetNewsCategoriesUseCase import com.tangem.domain.news.usecase.GetNewsListBatchFlowUseCase @@ -39,7 +41,7 @@ internal class NewsListModel @Inject constructor( ) : Model() { private val params = paramsContainer.require() - private val selectedCategoryId = MutableStateFlow(null) + private val selectedCategoryId = MutableStateFlow(params.preselectedCategoryId) private val categoriesLoader by lazy { NewsCategoriesLoader( @@ -67,7 +69,7 @@ internal class NewsListModel @Inject constructor( private val _state = MutableStateFlow( NewsListUM( - selectedCategoryId = DEFAULT_ALL_NEWS_CATEGORIES_ID, + selectedCategoryId = params.preselectedCategoryId ?: DEFAULT_ALL_NEWS_CATEGORIES_ID, filters = persistentListOf(), newsListState = NewsListState.Loading, listOfArticles = persistentListOf(), @@ -84,20 +86,38 @@ internal class NewsListModel @Inject constructor( val state = _state.asStateFlow() init { - loadCategories() observeNewsList() - batchFlowManager.reload() + loadCategories() } private fun loadCategories() { modelScope.launch(dispatchers.default) { val filterChips = categoriesLoader.load() + val validIds = filterChips.mapTo(mutableSetOf()) { it.id } + selectedCategoryId.value = selectedCategoryId.value?.takeIf { it in validIds } + val effectiveId = selectedCategoryId.value ?: DEFAULT_ALL_NEWS_CATEGORIES_ID + val scrollIndex = filterChips.indexOfFirst { it.id == effectiveId }.takeIf { it > 0 } _state.update { currentState -> - currentState.copy(filters = filterChips) + currentState.copy( + selectedCategoryId = effectiveId, + filters = filterChips.map { chip -> + chip.copy(isSelected = chip.id == effectiveId) + }.toImmutableList(), + scrollToCategoryEvent = if (scrollIndex != null) { + triggeredEvent(data = scrollIndex, onConsume = ::onScrollToCategoryConsumed) + } else { + currentState.scrollToCategoryEvent + }, + ) } + batchFlowManager.reload() } } + private fun onScrollToCategoryConsumed() { + _state.update { it.copy(scrollToCategoryEvent = consumedEvent()) } + } + private fun observeNewsList() { modelScope.launch(dispatchers.default) { combine( @@ -111,7 +131,6 @@ internal class NewsListModel @Inject constructor( paginationStatus = paginationStatus, onRetryClick = { loadCategories() - batchFlowManager.reload() }, onLoadMore = { batchFlowManager.loadMore() }, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt index ab551d070a..9bc099b815 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt @@ -3,6 +3,7 @@ package com.tangem.features.feed.ui.news.list import androidx.compose.animation.core.EaseOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -20,6 +21,7 @@ import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.ds.tabs.TangemTab +import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.* import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM @@ -50,6 +52,9 @@ internal fun NewsListContent(contentPadding: PaddingValues, state: NewsListUM, m internal fun NewsListContentV1(contentPadding: PaddingValues, state: NewsListUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value val lazyListState = rememberLazyListState() + val chipsListState = rememberLazyListState() + + ScrollChipsToSelected(state = state, chipsListState = chipsListState) Column( modifier = modifier @@ -58,6 +63,7 @@ internal fun NewsListContentV1(contentPadding: PaddingValues, state: NewsListUM, ) { SpacerH(contentPadding.calculateTopPadding()) LazyRow( + state = chipsListState, contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -84,9 +90,12 @@ internal fun NewsListContentV1(contentPadding: PaddingValues, state: NewsListUM, internal fun NewsListContentV2(contentPadding: PaddingValues, state: NewsListUM) { val background = LocalMainBottomSheetColor.current.value val lazyListState = rememberLazyListState() + val chipsListState = rememberLazyListState() var chipsHeight by remember { mutableStateOf(0.dp) } val density = LocalDensity.current + ScrollChipsToSelected(state = state, chipsListState = chipsListState) + Box( modifier = Modifier .fillMaxSize() @@ -103,6 +112,7 @@ internal fun NewsListContentV2(contentPadding: PaddingValues, state: NewsListUM) onArticleClick = state.onArticleClick, ) LazyRow( + state = chipsListState, modifier = Modifier .align(Alignment.TopStart) .padding(top = contentPadding.calculateTopPadding(), bottom = TangemTheme.dimens2.x4) @@ -141,6 +151,13 @@ internal fun NewsListContentV2(contentPadding: PaddingValues, state: NewsListUM) } } +@Composable +private fun ScrollChipsToSelected(state: NewsListUM, chipsListState: LazyListState) { + EventEffect(event = state.scrollToCategoryEvent) { index -> + chipsListState.animateScrollToItem(index) + } +} + @Suppress("LongMethod") @Preview(showBackground = true) @Composable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt index 9d8c0e32b1..b910d4f30d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt @@ -1,8 +1,10 @@ package com.tangem.features.feed.ui.news.list.state import androidx.compose.runtime.Immutable -import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.chip.entity.ChipUM +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import kotlinx.collections.immutable.ImmutableList @Immutable @@ -13,6 +15,7 @@ data class NewsListUM( val newsListState: NewsListState, val onArticleClick: (Int) -> Unit, val onBackClick: () -> Unit, + val scrollToCategoryEvent: StateEvent = consumedEvent(), ) @Immutable diff --git a/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDeepLinkHandlerTest.kt b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDeepLinkHandlerTest.kt new file mode 100644 index 0000000000..d1113f142f --- /dev/null +++ b/features/feed/impl/src/test/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDeepLinkHandlerTest.kt @@ -0,0 +1,80 @@ +package com.tangem.features.feed.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.deeplink.DeeplinkConst.CATEGORY_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.NEWS_ID_KEY +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class DefaultNewsDeepLinkHandlerTest { + + private val appRouter: AppRouter = mockk() + + @BeforeEach + fun setUp() { + every { appRouter.push(any(), any()) } just Runs + } + + @Test + fun `no params opens news list with null category`() { + DefaultNewsDeepLinkHandler(queryParams = emptyMap(), appRouter = appRouter) + + verify { appRouter.push(AppRoute.News(categoryId = null), any()) } + } + + @Test + fun `valid categoryId opens news list with that category`() { + DefaultNewsDeepLinkHandler( + queryParams = mapOf(CATEGORY_ID_KEY to "5"), + appRouter = appRouter, + ) + + verify { appRouter.push(AppRoute.News(categoryId = 5), any()) } + } + + @Test + fun `unknown categoryId is forwarded — sanitized in NewsListModel against loaded chips`() { + DefaultNewsDeepLinkHandler( + queryParams = mapOf(CATEGORY_ID_KEY to "42"), + appRouter = appRouter, + ) + + verify { appRouter.push(AppRoute.News(categoryId = 42), any()) } + } + + @Test + fun `non-integer categoryId is silently dropped`() { + DefaultNewsDeepLinkHandler( + queryParams = mapOf(CATEGORY_ID_KEY to "not-a-number"), + appRouter = appRouter, + ) + + verify { appRouter.push(AppRoute.News(categoryId = null), any()) } + } + + @Test + fun `newsId opens news details`() { + DefaultNewsDeepLinkHandler( + queryParams = mapOf(NEWS_ID_KEY to "20533"), + appRouter = appRouter, + ) + + verify { appRouter.push(AppRoute.NewsDetails(newsId = 20533), any()) } + } + + @Test + fun `newsId takes priority over categoryId`() { + DefaultNewsDeepLinkHandler( + queryParams = mapOf(NEWS_ID_KEY to "20533", CATEGORY_ID_KEY to "5"), + appRouter = appRouter, + ) + + verify { appRouter.push(AppRoute.NewsDetails(newsId = 20533), any()) } + } +} \ No newline at end of file