Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-06 09:49:59 +02:00
parent 843ec3d04e
commit 7a197ec6c9
24 changed files with 499 additions and 235 deletions

View file

@ -12,9 +12,11 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalPowerSavingState
import com.tangem.core.ui.res.TangemTheme
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.HazeTint
@ -62,7 +64,11 @@ fun BottomFade(gradientBrush: Brush, modifier: Modifier = Modifier) {
* but with a vertical blur.
*/
@Composable
fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) {
fun BottomFadeWithBlur(
backgroundColor: Color,
modifier: Modifier = Modifier,
hazeState: HazeState = LocalHazeState.current,
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val isPowerSavingState by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
@ -76,19 +82,9 @@ fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) {
.fillMaxWidth()
.height(TangemTheme.dimens.size100 + bottomBarHeight)
.hazeEffectTangem(
style = HazeStyle(
blurRadius = 20.dp,
tint = HazeTint(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
backgroundColor,
),
),
),
backgroundColor = Color.Transparent,
),
state = hazeState,
) {
blurRadius = 20.dp
progressive = HazeProgressive.verticalGradient(
startIntensity = 0f,
endIntensity = 1f,

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.decompose
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
@ -27,7 +28,9 @@ interface ComposableModularBottomSheetContentComponent {
* Renders the main content of the bottom sheet.
* @param bottomSheetState The current state of the bottom sheet. Useful for tracking visibility
* (e.g., for analytics or lifecycle effects when the sheet is [BottomSheetState.EXPANDED]).
* @param contentPadding Padding to apply as inner scroll offset in scrollable containers,
* allowing content to scroll under an overlaying top bar. Defaults to no padding.
*/
@Composable
fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier)
fun Content(bottomSheetState: State<BottomSheetState>, contentPadding: PaddingValues, modifier: Modifier)
}

View file

@ -1,10 +1,7 @@
package com.tangem.domain.search.model
import com.tangem.domain.markets.TokenMarket
data class SearchResult(
val textHints: List<SearchTextHint>,
val recentTokens: List<RecentSearchToken>,
val userAssets: List<UserAssetSearchEntry>,
val marketTokens: List<TokenMarket>,
)

View file

@ -3,7 +3,6 @@ package com.tangem.domain.search.usecase
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -13,15 +12,14 @@ import com.tangem.domain.search.model.UserAssetSearchEntry
import com.tangem.domain.search.repository.SearchRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
/**
* Primary search use case that produces [SearchResult] based on the current query.
*
* Behavior depends on the query:
* - **Empty query** returns search history: text hints and recently viewed tokens.
* - **Non-empty query** performs the search across all unlocked user wallets,
* matching currencies by name or symbol, and combines the results with externally provided market tokens.
* - **Non-empty query** performs the search across all unlocked user wallets, matching currencies by name or symbol.
*
* @property searchRepository local search history storage
* @property multiAccountStatusListSupplier supplier for loaded account status lists across all wallets
@ -37,16 +35,12 @@ class GetSearchResultsUseCase(
* Produces a [Flow] of [SearchResult] for the given [query].
*
* @param query the search query string; blank means "show history"
* @param marketTokens external flow of market token search results (provided by presentation layer)
*/
operator fun invoke(
query: String,
marketTokens: Flow<List<TokenMarket>> = flowOf(emptyList()),
): Flow<SearchResult> {
operator fun invoke(query: String): Flow<SearchResult> {
return if (query.isBlank()) {
observeHistory()
} else {
searchAssets(query, marketTokens)
observeUserAssets(query)
}
}
@ -59,26 +53,11 @@ class GetSearchResultsUseCase(
textHints = hints,
recentTokens = tokens,
userAssets = emptyList(),
marketTokens = emptyList(),
)
}
}
private fun searchAssets(query: String, marketTokens: Flow<List<TokenMarket>>): Flow<SearchResult> {
return combine(
observeUserAssets(query),
marketTokens,
) { userAssets, markets ->
SearchResult(
textHints = emptyList(),
recentTokens = emptyList(),
userAssets = userAssets,
marketTokens = markets,
)
}
}
private fun observeUserAssets(query: String): Flow<List<UserAssetSearchEntry>> {
private fun observeUserAssets(query: String): Flow<SearchResult> {
val lowerQuery = query.lowercase()
return combine(
multiAccountStatusListSupplier(),
@ -94,6 +73,12 @@ class GetSearchResultsUseCase(
statusLists
.filter { it.userWalletId in unlockedWallets }
.flatMap { statusList -> extractMatchingAssets(statusList, unlockedWallets, lowerQuery) }
}.map { userAssets ->
SearchResult(
textHints = emptyList(),
recentTokens = emptyList(),
userAssets = userAssets,
)
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.features.feed.components.earn
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
@ -23,6 +25,7 @@ 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.components.haze.hazeEffectTangem
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.extensions.clickableSingle
@ -35,6 +38,7 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor
import com.tangem.features.feed.model.earn.EarnModel
import com.tangem.features.feed.ui.components.FeedSearchBar
import com.tangem.features.feed.ui.earn.EarnContent
import dev.chrisbanes.haze.HazeProgressive
internal class DefaultEarnComponent(
appComponentContext: AppComponentContext,
@ -59,7 +63,16 @@ internal class DefaultEarnComponent(
FeedSearchBar(
isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
feedListSearchBar = state.feedListSearchBar,
modifier = Modifier.drawBehind { drawRect(background) },
modifier = Modifier
.drawBehind { drawRect(background) }
.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
@ -93,11 +106,16 @@ internal class DefaultEarnComponent(
}
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
override fun Content(
bottomSheetState: State<BottomSheetState>,
contentPadding: PaddingValues,
modifier: Modifier,
) {
val bottomSheet by bottomSheetSlot.subscribeAsState()
val state by earnModel.state.collectAsStateWithLifecycle()
EarnContent(
contentPadding = contentPadding,
state = state,
modifier = modifier,
)

View file

@ -1,5 +1,6 @@
package com.tangem.features.feed.components.feed
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
@ -18,14 +19,14 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.decompose.EmptyComposableBottomSheetComponent
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent.Params
import com.tangem.features.feed.model.feed.FeedComponentModel
import com.tangem.features.feed.model.feed.FeedModelClickIntents
import com.tangem.features.feed.ui.feed.FeedList
import com.tangem.features.feed.ui.feed.FeedListHeader
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
internal class DefaultFeedComponent(
appComponentContext: AppComponentContext,
@ -64,7 +65,11 @@ internal class DefaultFeedComponent(
}
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
override fun Content(
bottomSheetState: State<BottomSheetState>,
contentPadding: PaddingValues,
modifier: Modifier,
) {
LifecycleStartEffect(Unit) {
feedComponentModel.isVisibleOnScreen.value = true
onStopOrDispose {
@ -78,6 +83,7 @@ internal class DefaultFeedComponent(
modifier = modifier,
state = state,
promoBannersBlockComponent = promoBannersBlockComponent,
contentPadding = contentPadding,
)
bottomSheet.child?.instance?.BottomSheet()
}

View file

@ -1,6 +1,8 @@
package com.tangem.features.feed.components.market.details
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
@ -21,6 +23,7 @@ import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
@ -37,6 +40,7 @@ import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnal
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTopBar
import dev.chrisbanes.haze.HazeProgressive
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@ -101,6 +105,14 @@ internal class DefaultMarketsTokenDetailsComponent(
val background = LocalMainBottomSheetColor.current.value
if (LocalRedesignEnabled.current) {
TangemTopBar(
modifier = Modifier.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
@ -153,7 +165,11 @@ internal class DefaultMarketsTokenDetailsComponent(
}
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
override fun Content(
bottomSheetState: State<BottomSheetState>,
contentPadding: PaddingValues,
modifier: Modifier,
) {
LifecycleStartEffect(Unit) {
model.isVisibleOnScreen.value = true
onStopOrDispose {
@ -167,6 +183,7 @@ internal class DefaultMarketsTokenDetailsComponent(
}
MarketsTokenDetailsContent(
contentPadding = contentPadding,
modifier = modifier,
backgroundColor = LocalMainBottomSheetColor.current.value,
state = state,

View file

@ -1,6 +1,8 @@
package com.tangem.features.feed.components.market.list
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
@ -19,6 +21,7 @@ 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.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.res.LocalMainBottomSheetColor
@ -31,6 +34,7 @@ import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.ui.components.FeedSearchBar
import com.tangem.features.feed.ui.market.list.MarketsList
import com.tangem.features.feed.ui.market.list.TopBarWithSearch
import dev.chrisbanes.haze.HazeProgressive
import kotlinx.serialization.Serializable
internal class DefaultMarketsTokenListComponent(
@ -56,7 +60,16 @@ internal class DefaultMarketsTokenListComponent(
FeedSearchBar(
isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
feedListSearchBar = state.feedListSearchBar,
modifier = Modifier.drawBehind { drawRect(background) },
modifier = Modifier
.drawBehind { drawRect(background) }
.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
@ -87,7 +100,11 @@ internal class DefaultMarketsTokenListComponent(
}
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
override fun Content(
bottomSheetState: State<BottomSheetState>,
contentPadding: PaddingValues,
modifier: Modifier,
) {
LifecycleStartEffect(Unit) {
model.isVisibleOnScreen.value = true
onStopOrDispose {
@ -103,6 +120,7 @@ internal class DefaultMarketsTokenListComponent(
}
MarketsList(
contentPadding = contentPadding,
modifier = modifier,
state = state,
)

View file

@ -1,6 +1,8 @@
package com.tangem.features.feed.components.news.details
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
@ -18,6 +20,7 @@ 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.components.haze.hazeEffectTangem
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
@ -31,6 +34,7 @@ import com.tangem.domain.news.model.NewsListConfig
import com.tangem.features.feed.model.news.details.NewsDetailsModel
import com.tangem.features.feed.ui.news.details.NewsDetailsContent
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
import dev.chrisbanes.haze.HazeProgressive
import kotlinx.serialization.Serializable
internal class DefaultNewsDetailsComponent(
@ -46,6 +50,14 @@ internal class DefaultNewsDetailsComponent(
val state by newsDetailsModel.state.collectAsStateWithLifecycle()
if (LocalRedesignEnabled.current) {
TangemTopBar(
modifier = Modifier.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
type = TangemTopBarType.BottomSheet,
startContent = {
Icon(
@ -88,7 +100,6 @@ internal class DefaultNewsDetailsComponent(
} else {
TangemTopAppBar(
containerColor = background,
title = null,
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_back_24,
onClicked = state.onBackClick,
@ -105,11 +116,16 @@ internal class DefaultNewsDetailsComponent(
}
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
override fun Content(
bottomSheetState: State<BottomSheetState>,
contentPadding: PaddingValues,
modifier: Modifier,
) {
val state by newsDetailsModel.state.collectAsStateWithLifecycle()
NewsDetailsContent(
state = state,
modifier = modifier,
contentPadding = contentPadding,
)
}

View file

@ -1,6 +1,8 @@
package com.tangem.features.feed.components.news.list
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
@ -18,6 +20,7 @@ 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.components.haze.hazeEffectTangem
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
@ -30,6 +33,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.features.feed.model.news.list.NewsListModel
import com.tangem.features.feed.ui.news.list.NewsListContent
import dev.chrisbanes.haze.HazeProgressive
import kotlinx.serialization.Serializable
internal class DefaultNewsListComponent(
@ -45,6 +49,14 @@ internal class DefaultNewsListComponent(
val state by newsListModel.state.collectAsStateWithLifecycle()
if (LocalRedesignEnabled.current) {
TangemTopBar(
modifier = Modifier.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
title = resourceReference(R.string.common_news),
type = TangemTopBarType.BottomSheet,
startContent = {
@ -80,11 +92,16 @@ internal class DefaultNewsListComponent(
}
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
override fun Content(
bottomSheetState: State<BottomSheetState>,
contentPadding: PaddingValues,
modifier: Modifier,
) {
val state by newsListModel.state.collectAsStateWithLifecycle()
NewsListContent(
state = state,
modifier = modifier,
contentPadding = contentPadding,
)
}

View file

@ -1,5 +1,7 @@
package com.tangem.features.feed.components.search
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@ -7,6 +9,7 @@ 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.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.field.search.TangemFieldShape
import com.tangem.core.ui.ds.field.search.TangemSearchField
@ -15,6 +18,7 @@ import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.features.feed.model.search.SearchModel
import com.tangem.features.feed.ui.search.SearchContent
import com.tangem.features.feed.ui.search.state.SearchCallbacks
import dev.chrisbanes.haze.HazeProgressive
internal class DefaultSearchComponent(
appComponentContext: AppComponentContext,
@ -35,6 +39,14 @@ internal class DefaultSearchComponent(
}
TangemTopBar(
modifier = Modifier.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
type = TangemTopBarType.BottomSheet,
reserveSlotSpace = false,
content = {
@ -50,7 +62,11 @@ internal class DefaultSearchComponent(
}
@Composable
override fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier) {
override fun Content(
bottomSheetState: State<BottomSheetState>,
contentPadding: PaddingValues,
modifier: Modifier,
) {
val state by model.state.collectAsStateWithLifecycle()
val searchCallbacks = remember {
SearchCallbacks(
@ -64,6 +80,7 @@ internal class DefaultSearchComponent(
modifier = modifier,
content = state.content,
searchCallbacks = searchCallbacks,
contentPadding = contentPadding,
)
}

View file

@ -34,6 +34,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L
private const val MARKET_SEARCH_DEBOUNCE_MS = 500L
@Suppress("LongParameterList")
@ModelScoped
@ -52,6 +53,7 @@ internal class SearchModel @Inject constructor(
private val updateQuotesJob = JobHolder()
private val searchResultsJob = JobHolder()
private val marketSearchDebounceJob = JobHolder()
private var shouldShowAllTokensIncludingUnder100k = false
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
@ -145,13 +147,20 @@ internal class SearchModel @Inject constructor(
.onEach { query ->
shouldShowAllTokensIncludingUnder100k = false
if (query.isEmpty()) {
marketSearchDebounceJob.cancel()
searchMarketsListManager.clearStateAndStopAllActions()
updateQuotesJob.cancel()
loadHistory()
} else {
stateController.update(SetSearchResultsLoadingTransformer())
searchMarketsListManager.reload(searchText = query)
subscribeToSearchResults(query)
modelScope.launch(dispatchers.default) {
delay(MARKET_SEARCH_DEBOUNCE_MS)
val latestQuery = stateController.value.searchBar.query.trim()
if (latestQuery.isNotEmpty()) {
searchMarketsListManager.reload(searchText = latestQuery)
}
}.saveIn(marketSearchDebounceJob)
}
}
.launchIn(modelScope)
@ -159,10 +168,7 @@ internal class SearchModel @Inject constructor(
private fun subscribeToSearchResults(query: String) {
modelScope.launch {
getSearchResultsUseCase(
query = query,
marketTokens = searchMarketsListManager.rawItems,
).collectLatest { searchResult ->
getSearchResultsUseCase(query = query).collectLatest { searchResult ->
val userAssets = searchResult.userAssets.map { entry ->
UserAssetItemUM(
id = "${entry.userWalletId.stringValue}_${entry.accountId.value}" +

View file

@ -1,26 +1,30 @@
package com.tangem.features.feed.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.features.feed.components.FeedEntryChildFactory
import com.tangem.features.feed.ui.utils.contentFeedEntryAnimatedContentTransitionSpec
import com.tangem.features.feed.ui.utils.contentFeedEntryStackAnimation
import com.tangem.features.feed.ui.utils.topBarFeedEntryAnimatedContentTransitionSpec
import dev.chrisbanes.haze.rememberHazeState
@OptIn(ExperimentalDecomposeApi::class)
@Composable
@ -29,18 +33,41 @@ internal fun EntryContent(
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
onHeaderSizeChange: (Dp) -> Unit,
isOpenedInBottomSheet: Boolean,
) {
if (LocalRedesignEnabled.current) {
EntryContentV2(
bottomSheetState = bottomSheetState,
stackState = stackState,
onHeaderSizeChange = onHeaderSizeChange,
isOpenedInBottomSheet = isOpenedInBottomSheet,
)
} else {
EntryContentV1(
bottomSheetState = bottomSheetState,
stackState = stackState,
onHeaderSizeChange = onHeaderSizeChange,
isOpenedInBottomSheet = isOpenedInBottomSheet,
)
}
}
@Composable
private fun EntryContentV1(
bottomSheetState: State<BottomSheetState>,
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
onHeaderSizeChange: (Dp) -> Unit,
isOpenedInBottomSheet: Boolean,
) {
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
val animationContent = remember(stackState) { contentFeedEntryAnimatedContentTransitionSpec(stackState) }
val animationAppBar = remember(stackState) { topBarFeedEntryAnimatedContentTransitionSpec(stackState) }
val stackAnimation = remember { contentFeedEntryStackAnimation() }
Surface(contentColor = background) {
Scaffold(
containerColor = background,
contentWindowInsets = WindowInsetsZero,
topBar = {
AnimatedContent(
Children(
modifier = Modifier
.then(
if (!isOpenedInBottomSheet) {
@ -56,6 +83,77 @@ internal fun EntryContent(
}
}
},
stack = stackState.value,
animation = stackAnimation,
) { child ->
child.instance.Title(bottomSheetState)
}
},
content = { contentPadding ->
Children(
stack = stackState.value,
animation = stackAnimation,
) { child ->
child.instance.Content(
modifier = Modifier.padding(contentPadding),
bottomSheetState = bottomSheetState,
contentPadding = PaddingValues(),
)
}
},
)
}
}
@Composable
private fun EntryContentV2(
bottomSheetState: State<BottomSheetState>,
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
onHeaderSizeChange: (Dp) -> Unit,
isOpenedInBottomSheet: Boolean,
) {
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
val animationContent = remember { contentFeedEntryStackAnimation() }
val animationAppBar = remember(stackState) { topBarFeedEntryAnimatedContentTransitionSpec(stackState) }
var topBarHeight by remember { mutableStateOf(0.dp) }
val hazeState = rememberHazeState()
Surface(contentColor = background) {
CompositionLocalProvider(LocalHazeState provides hazeState) {
Box(modifier = Modifier.fillMaxSize()) {
Children(
modifier = Modifier.fillMaxSize(),
stack = stackState.value,
animation = animationContent,
) { child ->
child.instance.Content(
modifier = Modifier
.fillMaxSize()
.hazeSourceTangem(zIndex = 0f, state = hazeState),
contentPadding = PaddingValues(top = topBarHeight),
bottomSheetState = bottomSheetState,
)
}
AnimatedContent(
modifier = Modifier
.align(Alignment.TopStart)
.then(
if (!isOpenedInBottomSheet) {
Modifier.statusBarsPadding()
} else {
Modifier
},
)
.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {
with(density) {
val height = coordinates.size.height.toDp()
topBarHeight = height
onHeaderSizeChange(height)
}
}
},
targetState = stackState.value.active,
transitionSpec = animationAppBar,
contentKey = { it.key },
@ -63,20 +161,7 @@ internal fun EntryContent(
) { state ->
state.instance.Title(bottomSheetState)
}
},
content = { contentPadding ->
AnimatedContent(
targetState = stackState.value.active,
transitionSpec = animationContent,
contentKey = { it.key },
label = "FeedEntryContent",
) { state ->
state.instance.Content(
modifier = Modifier.padding(contentPadding),
bottomSheetState = bottomSheetState,
)
}
},
)
}
}
}
}

View file

@ -22,11 +22,7 @@ import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.list.InfiniteListHandler
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.*
import com.tangem.features.feed.ui.earn.components.*
import com.tangem.features.feed.ui.earn.state.*
@ -36,7 +32,7 @@ import kotlinx.collections.immutable.persistentListOf
private const val EARN_LOAD_MORE_BUFFER = 3
@Composable
internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
internal fun EarnContent(contentPadding: PaddingValues, state: EarnUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
@ -55,7 +51,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
modifier = modifier
.fillMaxSize()
.background(background),
contentPadding = PaddingValues(bottom = bottomBarHeight),
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
) {
item(key = "mostly_used_header") {
SectionHeader(
@ -395,6 +391,7 @@ private fun EarnContentPreviewV1() {
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
contentPadding = PaddingValues(),
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(
@ -437,6 +434,7 @@ private fun EarnContentPreviewV2() {
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
contentPadding = PaddingValues(),
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(
@ -479,6 +477,7 @@ private fun EarnContentLoadingPreviewV1() {
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
contentPadding = PaddingValues(),
state = previewEarnUM(
mostlyUsed = EarnListUM.Error(onRetryClicked = {}),
bestOpportunities = EarnBestOpportunitiesUM.Loading,
@ -498,6 +497,7 @@ private fun EarnContentLoadingPreviewV2() {
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
contentPadding = PaddingValues(),
state = previewEarnUM(
mostlyUsed = EarnListUM.Error(onRetryClicked = {}),
bestOpportunities = EarnBestOpportunitiesUM.Loading,
@ -517,6 +517,7 @@ private fun EarnContentErrorPreviewV1() {
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
contentPadding = PaddingValues(),
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(
@ -545,6 +546,7 @@ private fun EarnContentErrorPreviewV2() {
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
contentPadding = PaddingValues(),
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(
@ -573,6 +575,7 @@ private fun EarnContentEmptyPreviewV1() {
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
contentPadding = PaddingValues(),
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(
@ -601,6 +604,7 @@ private fun EarnContentEmptyPreviewV2() {
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
contentPadding = PaddingValues(),
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(

View file

@ -1,6 +1,7 @@
package com.tangem.features.feed.ui.feed
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
@ -14,8 +15,11 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BaseSearchBarTestTags.SEARCH_BAR
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
@ -25,6 +29,7 @@ import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.crea
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import com.tangem.features.feed.ui.feed.state.FeedListUM
import com.tangem.features.feed.ui.feed.state.GlobalFeedState
import dev.chrisbanes.haze.HazeProgressive
@Composable
internal fun FeedListHeader(
@ -38,12 +43,26 @@ internal fun FeedListHeader(
feedListSearchBar = feedListSearchBar,
modifier = modifier
.drawBehind { drawRect(background) }
.conditionalCompose(
condition = LocalRedesignEnabled.current,
modifier = {
hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .75f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
}
},
)
.testTag(SEARCH_BAR),
)
}
@Composable
internal fun FeedList(
contentPadding: PaddingValues,
state: FeedListUM,
modifier: Modifier = Modifier,
promoBannersBlockComponent: ComposableContentComponent? = null,
@ -59,19 +78,23 @@ internal fun FeedList(
FeedListLoading(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.drawBehind { drawRect(background) },
.drawBehind { drawRect(background) }
.padding(top = contentPadding.calculateTopPadding())
.verticalScroll(rememberScrollState()),
)
}
is GlobalFeedState.Error -> {
FeedListGlobalError(
onRetryClick = animatedState.onRetryClicked,
modifier = Modifier.drawBehind { drawRect(background) },
modifier = Modifier
.drawBehind { drawRect(background) }
.padding(top = contentPadding.calculateTopPadding()),
currentDate = state.currentDate,
)
}
is GlobalFeedState.Content -> {
FeedListContent(
contentPadding = contentPadding,
modifier = Modifier,
state = state,
promoBannersBlockComponent = promoBannersBlockComponent,
@ -83,6 +106,7 @@ internal fun FeedList(
@Composable
private fun FeedListContent(
contentPadding: PaddingValues,
state: FeedListUM,
modifier: Modifier = Modifier,
promoBannersBlockComponent: ComposableContentComponent? = null,
@ -95,6 +119,9 @@ private fun FeedListContent(
.padding(bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding())
.drawBehind { drawRect(background) },
) {
if (LocalRedesignEnabled.current) {
SpacerH(contentPadding.calculateTopPadding())
}
DateBlock(state.currentDate)
SpacerH(32.dp)
@ -127,6 +154,6 @@ private fun FeedListContent(
@Composable
private fun FeedListPreview() {
TangemThemePreview {
FeedList(state = createFeedPreviewState())
FeedList(state = createFeedPreviewState(), contentPadding = PaddingValues())
}
}

View file

@ -58,12 +58,14 @@ import com.tangem.core.ui.R as CoreR
@Suppress("LongParameterList")
@Composable
internal fun MarketsTokenDetailsContent(
contentPadding: PaddingValues,
state: MarketsTokenDetailsUM,
backgroundColor: Color,
modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
) {
Content(
contentPadding = contentPadding,
modifier = modifier,
backgroundColor = backgroundColor,
state = state,
@ -80,6 +82,7 @@ internal fun MarketsTokenDetailsContent(
@Suppress("LongParameterList")
@Composable
private fun Content(
contentPadding: PaddingValues,
state: MarketsTokenDetailsUM,
backgroundColor: Color,
modifier: Modifier = Modifier,
@ -103,7 +106,7 @@ private fun Content(
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(bottom = bottomBarHeight),
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
) {
item("header") {
Header(
@ -343,6 +346,7 @@ private fun MarketsTokenDetailsContent_Preview(
state = params,
backgroundColor = TangemTheme.colors.background.tertiary,
portfolioBlock = {},
contentPadding = PaddingValues(),
)
}
}

View file

@ -17,9 +17,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.markets.preview.MarketChartListItemPreviewDataProvider
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
@ -27,7 +25,6 @@ import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.resourceReference
@ -105,7 +102,7 @@ internal fun TopBarWithSearch(
}
@Composable
internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) {
internal fun MarketsList(contentPadding: PaddingValues, state: MarketsListUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
Column(
modifier = modifier
@ -113,7 +110,7 @@ internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) {
.imePadding()
.drawBehind { drawRect(background) },
) {
Content(state = state)
Content(state = state, contentPadding = contentPadding)
}
MarketsListSortByBottomSheet(config = state.sortByBottomSheet)
KeyboardEvents(isSortByBottomSheetShown = state.sortByBottomSheet.isShown)
@ -121,7 +118,7 @@ internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) {
@Suppress("LongMethod")
@Composable
private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modifier) {
private fun ColumnScope.Content(contentPadding: PaddingValues, state: MarketsListUM, modifier: Modifier = Modifier) {
val isRedesignEnabled = LocalRedesignEnabled.current
val hazeState = rememberHazeState()
@ -135,6 +132,7 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif
val scrolledState = remember { mutableStateOf(false) }
Column(modifier.padding(horizontal = TangemTheme.dimens.size16)) {
SpacerH(contentPadding.calculateTopPadding())
AnimatedVisibility(
visible = scrolledState.value.not() &&
state.isInSearchMode &&
@ -276,6 +274,7 @@ private fun Preview() {
LocalMainBottomSheetColor provides remember { mutableStateOf(primaryBackground) },
) {
MarketsList(
contentPadding = PaddingValues(),
state = MarketsListUM(
list = ListUM.Content(
items = MarketChartListItemPreviewDataProvider().values

View file

@ -24,7 +24,7 @@ import com.tangem.features.feed.ui.news.details.state.MockArticlesFactory
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
@Composable
internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modifier) {
internal fun NewsDetailsContent(state: NewsDetailsUM, contentPadding: PaddingValues, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
AnimatedContent(
targetState = state.articlesStateUM,
@ -32,15 +32,16 @@ internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modif
) { animatedState ->
when (animatedState) {
ArticlesStateUM.Content -> {
Content(state = state, background = background)
Content(state = state, background = background, contentPadding = contentPadding)
}
ArticlesStateUM.Loading -> {
NewsDetailsPlaceholder(background = background)
NewsDetailsPlaceholder(background = background, contentPadding = contentPadding)
}
is ArticlesStateUM.LoadingError -> {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = contentPadding.calculateTopPadding())
.background(background),
contentAlignment = Alignment.Center,
) {
@ -57,7 +58,7 @@ internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modif
}
@Composable
private fun Content(state: NewsDetailsUM, background: Color) {
private fun Content(contentPadding: PaddingValues, state: NewsDetailsUM, background: Color) {
val isRedesignEnabled = LocalRedesignEnabled.current
val pagerState = rememberPagerState(
initialPage = state.selectedArticleIndex,
@ -96,6 +97,7 @@ private fun Content(state: NewsDetailsUM, background: Color) {
modifier = Modifier.fillMaxSize(),
onLikeClick = { state.onLikeClick(article.id) },
relatedTokensUM = state.relatedTokensUM,
contentPadding = contentPadding,
)
}
if (state.articles.size > 1) {
@ -142,6 +144,7 @@ private fun PreviewNewsDetailsContent() {
onBackClick = {},
onArticleIndexChanged = {},
),
contentPadding = PaddingValues(),
)
}
}
@ -166,6 +169,7 @@ private fun PreviewNewsDetailsContentV2() {
onBackClick = {},
onArticleIndexChanged = {},
),
contentPadding = PaddingValues(),
)
}
}

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -28,6 +29,7 @@ import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
@ -35,9 +37,11 @@ import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader
import com.tangem.features.feed.ui.news.details.state.ArticleUM
import com.tangem.features.feed.ui.news.details.state.RelatedArticleUM
import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM
import dev.chrisbanes.haze.rememberHazeState
@Composable
internal fun ArticleDetail(
contentPadding: PaddingValues,
article: ArticleUM,
onLikeClick: () -> Unit,
relatedTokensUM: RelatedTokensUM,
@ -45,6 +49,7 @@ internal fun ArticleDetail(
) {
if (LocalRedesignEnabled.current) {
ArticleDetailV2(
contentPadding = contentPadding,
article = article,
onLikeClick = onLikeClick,
relatedTokensUM = relatedTokensUM,
@ -190,6 +195,7 @@ private fun ArticleDetailV1(
@Suppress("LongMethod")
@Composable
internal fun ArticleDetailV2(
contentPadding: PaddingValues,
article: ArticleUM,
onLikeClick: () -> Unit,
relatedTokensUM: RelatedTokensUM,
@ -199,126 +205,127 @@ internal fun ArticleDetailV2(
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
val pagerHeight = 32.dp
val contentPadding = pagerHeight + 56.dp + with(density) {
val bottomPadding = pagerHeight + 56.dp + with(density) {
WindowInsets.navigationBars.getBottom(this).div(this.density)
}.dp
Box(modifier = modifier) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.hazeSourceTangem(zIndex = 0f)
.background(background),
contentPadding = PaddingValues(bottom = contentPadding),
) {
item("content") {
ArticleHeader(
title = article.title,
createdAt = article.createdAt.resolveReference(),
score = article.score,
tags = article.tags,
isTrending = article.isTrending,
modifier = Modifier
.padding(top = 16.dp)
.padding(horizontal = 16.dp),
)
if (article.shortContent.isNotEmpty()) {
QuickRecap(
content = article.shortContent,
CompositionLocalProvider(LocalHazeState provides rememberHazeState()) {
Box(modifier = modifier) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.hazeSourceTangem(zIndex = -1f)
.background(background),
contentPadding = PaddingValues(bottom = bottomPadding, top = contentPadding.calculateTopPadding()),
) {
item("content") {
ArticleHeader(
title = article.title,
createdAt = article.createdAt.resolveReference(),
score = article.score,
tags = article.tags,
isTrending = article.isTrending,
modifier = Modifier
.padding(top = 32.dp)
.padding(top = 16.dp)
.padding(horizontal = 16.dp),
)
}
Text(
text = article.content,
style = TangemTheme.typography2.bodyRegular16,
color = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier
.padding(top = 12.dp)
.padding(horizontal = 16.dp),
)
SpacerH(24.dp)
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = TangemTheme.colors2.border.neutral.primary,
)
SpacerH(20.dp)
SecondaryTangemButton(
modifier = Modifier.padding(horizontal = 24.dp),
text = resourceReference(R.string.news_like),
size = com.tangem.core.ui.ds.button.TangemButtonSize.X9,
iconRes = if (article.isLiked) {
R.drawable.ic_like_20
} else {
R.drawable.ic_heart_20
},
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onLikeClick()
},
shape = TangemButtonShape.Rounded,
)
RelatedTokensBlock(
relatedTokensUM = relatedTokensUM,
onItemClick = when (relatedTokensUM) {
is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick
else -> null
},
modifier = Modifier.padding(horizontal = 16.dp),
)
if (article.relatedArticles.isNotEmpty()) {
SpacerH(24.dp)
Row(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringResourceSafe(R.string.news_sources),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
if (article.shortContent.isNotEmpty()) {
QuickRecap(
content = article.shortContent,
modifier = Modifier
.padding(top = 32.dp)
.padding(horizontal = 16.dp),
)
}
}
}
if (article.relatedArticles.isNotEmpty()) {
item("relatedArticles") {
LazyRow(
Text(
text = article.content,
style = TangemTheme.typography2.bodyRegular16,
color = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier
.padding(vertical = 12.dp),
state = rememberLazyListState(),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(
items = article.relatedArticles,
key = RelatedArticleUM::id,
) { article ->
RelatedNewsItem(
relatedArticle = article,
modifier = Modifier.fillParentMaxHeight(),
.padding(top = 12.dp)
.padding(horizontal = 16.dp),
)
SpacerH(24.dp)
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = TangemTheme.colors2.border.neutral.primary,
)
SpacerH(20.dp)
SecondaryTangemButton(
modifier = Modifier.padding(horizontal = 24.dp),
text = resourceReference(R.string.news_like),
size = com.tangem.core.ui.ds.button.TangemButtonSize.X9,
iconRes = if (article.isLiked) {
R.drawable.ic_like_20
} else {
R.drawable.ic_heart_20
},
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onLikeClick()
},
shape = TangemButtonShape.Rounded,
)
RelatedTokensBlock(
relatedTokensUM = relatedTokensUM,
onItemClick = when (relatedTokensUM) {
is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick
else -> null
},
modifier = Modifier.padding(horizontal = 16.dp),
)
if (article.relatedArticles.isNotEmpty()) {
SpacerH(24.dp)
Row(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringResourceSafe(R.string.news_sources),
style = TangemTheme.typography2.headingSemibold20,
color = TangemTheme.colors2.text.neutral.primary,
)
}
}
}
}
}
BottomFadeWithBlur(
modifier = Modifier
.align(Alignment.BottomCenter)
.height(80.dp)
.fillMaxWidth(),
backgroundColor = background,
)
if (article.relatedArticles.isNotEmpty()) {
item("relatedArticles") {
LazyRow(
modifier = Modifier.padding(vertical = 12.dp),
state = rememberLazyListState(),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
items(
items = article.relatedArticles,
key = RelatedArticleUM::id,
) { article ->
RelatedNewsItem(
relatedArticle = article,
modifier = Modifier.fillParentMaxHeight(),
)
}
}
}
}
}
BottomFadeWithBlur(
modifier = Modifier
.align(Alignment.BottomCenter)
.height(80.dp)
.fillMaxWidth(),
backgroundColor = background,
)
}
}
}

View file

@ -17,9 +17,9 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
fun NewsDetailsPlaceholder(background: Color, modifier: Modifier = Modifier) {
fun NewsDetailsPlaceholder(contentPadding: PaddingValues, background: Color, modifier: Modifier = Modifier) {
if (LocalRedesignEnabled.current) {
NewsDetailsPlaceholderV2(background, modifier)
NewsDetailsPlaceholderV2(contentPadding, background, modifier)
} else {
NewsDetailsPlaceholderV1(background, modifier)
}
@ -103,13 +103,14 @@ private fun NewsDetailsPlaceholderV1(background: Color, modifier: Modifier = Mod
@Suppress("LongMethod")
@Composable
private fun NewsDetailsPlaceholderV2(background: Color, modifier: Modifier = Modifier) {
private fun NewsDetailsPlaceholderV2(contentPadding: PaddingValues, background: Color, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.background(background)
.padding(16.dp),
) {
SpacerH(contentPadding.calculateTopPadding())
Row(
modifier = Modifier.height(50.dp),
horizontalArrangement = Arrangement.spacedBy(30.dp),
@ -214,7 +215,10 @@ private fun NewsDetailsPlaceholderV2(background: Color, modifier: Modifier = Mod
@Composable
private fun NewsDetailsPlaceholderPreviewV1() {
TangemThemePreview {
NewsDetailsPlaceholder(background = TangemTheme.colors.background.tertiary)
NewsDetailsPlaceholder(
background = TangemTheme.colors.background.tertiary,
contentPadding = PaddingValues(),
)
}
}
@ -223,6 +227,9 @@ private fun NewsDetailsPlaceholderPreviewV1() {
@Composable
private fun NewsDetailsPlaceholderPreviewV2() {
TangemThemePreviewRedesign {
NewsDetailsPlaceholder(background = TangemTheme.colors2.surface.level3)
NewsDetailsPlaceholder(
background = TangemTheme.colors2.surface.level3,
contentPadding = PaddingValues(),
)
}
}

View file

@ -12,7 +12,6 @@ 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.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.chip.Chip
import com.tangem.core.ui.components.chip.entity.ChipUM
@ -22,6 +21,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.features.feed.ui.news.list.components.NewsListLazyColumn
import com.tangem.features.feed.ui.news.list.state.NewsListState
import com.tangem.features.feed.ui.news.list.state.NewsListUM
@ -29,7 +29,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableSet
@Composable
internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) {
internal fun NewsListContent(contentPadding: PaddingValues, state: NewsListUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
val isRedesignEnabled = LocalRedesignEnabled.current
val lazyListState = rememberLazyListState()
@ -39,6 +39,7 @@ internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) {
.fillMaxSize()
.background(background),
) {
SpacerH(contentPadding.calculateTopPadding())
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
@ -149,6 +150,7 @@ private fun NewsListContentPreview() {
onArticleClick = {},
onBackClick = {},
),
contentPadding = PaddingValues(),
)
}
}

View file

@ -17,6 +17,7 @@ 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.draw.drawBehind
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
@ -34,6 +35,7 @@ import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.search.state.*
@ -41,17 +43,26 @@ private const val PLACEHOLDER_COUNT = 10
private const val LOAD_MORE_THRESHOLD = 5
@Composable
internal fun SearchContent(content: SearchContentUM, searchCallbacks: SearchCallbacks, modifier: Modifier = Modifier) {
internal fun SearchContent(
content: SearchContentUM,
searchCallbacks: SearchCallbacks,
contentPadding: PaddingValues,
modifier: Modifier = Modifier,
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val lazyListState = rememberLazyListState()
val background = LocalMainBottomSheetColor.current.value
LazyColumn(
state = lazyListState,
modifier = modifier.fillMaxSize(),
modifier = modifier
.fillMaxSize()
.drawBehind { drawRect(background) },
contentPadding = PaddingValues(
start = TangemTheme.dimens2.x4,
end = TangemTheme.dimens2.x4,
bottom = bottomBarHeight,
top = contentPadding.calculateTopPadding(),
),
) {
when (content) {

View file

@ -3,6 +3,7 @@ package com.tangem.features.feed.ui.search.preview
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -408,6 +409,7 @@ private fun SearchContentPreviewHost(
content = scenario.content,
searchCallbacks = SearchContentPreviewCallbacks,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(),
)
}
}

View file

@ -4,15 +4,16 @@ import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.runtime.State
import com.arkivanov.decompose.Child
import com.arkivanov.decompose.FaultyDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.animation.StackAnimation
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.features.feed.components.FeedEntryChildFactory
private fun FeedEntryChildFactory.Child?.usesFadeStackTransition(): Boolean = when (this) {
is FeedEntryChildFactory.Child.Search -> true
is FeedEntryChildFactory.Child.TokenList -> params.shouldAlwaysShowSearchBar
else -> false
}
private const val FEED_ENTRY_SLIDE_DURATION_MS = 300
internal typealias FeedEntryActiveChild =
Child.Created<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>
@ -20,12 +21,24 @@ internal typealias FeedEntryActiveChild =
internal typealias FeedEntryChildStack =
ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>
internal fun topBarFeedEntryAnimatedContentTransitionSpec(
stackState: State<FeedEntryChildStack>,
): AnimatedContentTransitionScope<FeedEntryActiveChild>.() -> ContentTransform =
{ feedEntryAnimatedContentTransform(stackState.value) }
@OptIn(FaultyDecomposeApi::class)
internal fun contentFeedEntryStackAnimation(): StackAnimation<
FeedEntryChildFactory.Child,
ComposableModularBottomSheetContentComponent,
> =
stackAnimation { to, from, _ ->
val isSearchToTokenList =
(to.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
val isFromSearchTokenList =
(from.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true
if (isSearchToTokenList || isFromSearchTokenList) {
fade()
} else {
slide()
}
}
internal fun contentFeedEntryAnimatedContentTransitionSpec(
internal fun topBarFeedEntryAnimatedContentTransitionSpec(
stackState: State<FeedEntryChildStack>,
): AnimatedContentTransitionScope<FeedEntryActiveChild>.() -> ContentTransform =
{ feedEntryAnimatedContentTransform(stackState.value) }
@ -36,8 +49,8 @@ private fun AnimatedContentTransitionScope<FeedEntryActiveChild>.feedEntryAnimat
val shouldUseFade = initialState.configuration.usesFadeStackTransition() ||
targetState.configuration.usesFadeStackTransition()
return if (shouldUseFade) {
fadeIn(animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS)) togetherWith
fadeOut(animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS))
// No transition: fade/slide both fight with haze during the animation frame window.
EnterTransition.None togetherWith ExitTransition.None
} else {
feedEntrySlideTransform(stack)
}
@ -66,5 +79,8 @@ private fun AnimatedContentTransitionScope<FeedEntryActiveChild>.feedEntrySlideT
}
}
private const val FEED_ENTRY_FADE_DURATION_MS = 300
private const val FEED_ENTRY_SLIDE_DURATION_MS = 300
private fun FeedEntryChildFactory.Child?.usesFadeStackTransition(): Boolean = when (this) {
is FeedEntryChildFactory.Child.Search -> true
is FeedEntryChildFactory.Child.TokenList -> params.shouldAlwaysShowSearchBar
else -> false
}