Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-21 17:59:40 +05:00
commit a22e77ebe8
669 changed files with 27695 additions and 7905 deletions

View file

@ -17,9 +17,9 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PreselectedMarketsInterval
import com.tangem.domain.markets.PreselectedMarketsOrder
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.earn.PreselectedEarnType
import com.tangem.domain.news.model.NewsListConfig
@ -32,8 +32,6 @@ import com.tangem.features.feed.entry.components.FeedEntryComponent
import com.tangem.features.feed.entry.components.FeedEntryRoute
import com.tangem.features.feed.model.FeedEntryModel
import com.tangem.features.feed.model.feed.FeedModelClickIntents
import com.tangem.domain.markets.PreselectedMarketsInterval
import com.tangem.domain.markets.PreselectedMarketsOrder
import com.tangem.features.feed.model.market.list.state.MarketsListUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.ui.EntryContent
@ -199,26 +197,21 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val background = TangemTheme.colors.background.tertiary
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember(background) { mutableStateOf(background) },
) {
val bottomSheetState = remember {
derivedStateOf { BottomSheetState.EXPANDED }
}
BackHandler {
router.pop()
}
EntryContent(
bottomSheetState = bottomSheetState,
stackState = stack.subscribeAsState(),
onHeaderSizeChange = {},
onExpandSheet = {},
isOpenedInBottomSheet = false,
)
val bottomSheetState = remember {
derivedStateOf { BottomSheetState.EXPANDED }
}
BackHandler {
router.pop()
}
EntryContent(
bottomSheetState = bottomSheetState,
stackState = stack.subscribeAsState(),
onHeaderSizeChange = {},
onExpandSheet = {},
isOpenedInBottomSheet = false,
)
}
private fun onChildBack() {

View file

@ -18,19 +18,16 @@ import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListCo
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
import com.tangem.features.feed.components.search.DefaultSearchComponent
import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import kotlinx.serialization.Serializable
import javax.inject.Inject
@Suppress("LongParameterList")
internal class FeedEntryChildFactory @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val portfolioComponentFactory: MarketsPortfolioComponent.Factory,
private val portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles,
private val designFeatureToggles: DesignFeatureToggles,
) {
@ -121,7 +118,6 @@ internal class FeedEntryChildFactory @Inject constructor(
params = FeedParams(feedClickIntents = feedEntryClickIntents),
addToPortfolioComponentFactory = addToPortfolioComponentFactory,
promoBannersBlockComponentFactory = promoBannersBlockComponentFactory,
newPromoBannersFeatureToggles = newPromoBannersFeatureToggles,
)
}
is Child.Earn -> {

View file

@ -1,7 +1,5 @@
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
@ -11,9 +9,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
@ -39,7 +38,6 @@ import com.tangem.features.feed.components.feed.FeedBottomSheetRoute
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
import kotlinx.serialization.Serializable
internal class DefaultEarnComponent(
@ -65,16 +63,6 @@ internal class DefaultEarnComponent(
FeedSearchBar(
isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
feedListSearchBar = state.feedListSearchBar,
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),
@ -82,10 +70,8 @@ internal class DefaultEarnComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = state.onBackClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,

View file

@ -24,7 +24,6 @@ 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(
@ -32,13 +31,11 @@ internal class DefaultFeedComponent(
private val params: FeedParams,
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles,
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
private val feedComponentModel = getOrCreateModel<FeedComponentModel, FeedParams>(params = params)
private val promoBannersBlockComponent: PromoBannersBlockComponent? by lazy {
if (!newPromoBannersFeatureToggles.isNewPromoBannersEnabled) return@lazy null
private val promoBannersBlockComponent: PromoBannersBlockComponent by lazy {
promoBannersBlockComponentFactory.create(
context = child("promoBannersBlockComponent"),
params = PromoBannersBlockComponent.Params(
@ -72,7 +69,7 @@ internal class DefaultFeedComponent(
) {
val isExpanded = bottomSheetState.value == BottomSheetState.EXPANDED
LaunchedEffect(isExpanded) {
promoBannersBlockComponent?.setVisibleOnScreen(isExpanded)
promoBannersBlockComponent.setVisibleOnScreen(isExpanded)
}
LifecycleStartEffect(Unit) {

View file

@ -1,7 +1,5 @@
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
@ -12,8 +10,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
@ -48,9 +48,9 @@ import com.tangem.features.feed.components.market.details.portfolioblock.Portfol
import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel
import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
import com.tangem.features.feed.ui.LocalIsOpenedInBottomSheet
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
@ -161,14 +161,6 @@ 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),
@ -176,10 +168,8 @@ internal class DefaultMarketsTokenDetailsComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = { params.onBackClicked() },
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
@ -194,10 +184,8 @@ internal class DefaultMarketsTokenDetailsComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = state.onShareClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
@ -205,7 +193,11 @@ internal class DefaultMarketsTokenDetailsComponent(
.padding(TangemTheme.dimens2.x2_5),
)
},
type = TangemTopBarType.BottomSheet,
type = if (LocalIsOpenedInBottomSheet.current) {
TangemTopBarType.BottomSheet
} else {
TangemTopBarType.Default
},
)
} else {
MarketsTokenDetailsTopBar(

View file

@ -11,9 +11,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
@ -34,7 +35,6 @@ 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(
@ -54,21 +54,13 @@ internal class DefaultMarketsTokenListComponent(
override fun Title(bottomSheetState: State<BottomSheetState>) {
val state by model.state.collectAsStateWithLifecycle()
val bsState by bottomSheetState
val background = LocalMainBottomSheetColor.current.value
if (LocalRedesignEnabled.current) {
val background = LocalMainBottomSheetColor.current.value
FeedSearchBar(
isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
feedListSearchBar = state.feedListSearchBar,
modifier = Modifier
.drawBehind { drawRect(background) }
.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = .2f,
preferPerformance = true,
)
},
modifier = Modifier.background(background.copy(alpha = .95f)),
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
@ -76,10 +68,8 @@ internal class DefaultMarketsTokenListComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = clickIntents.onBackClicked,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,

View file

@ -1,7 +1,5 @@
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
@ -11,8 +9,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
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
@ -32,9 +32,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.features.feed.model.news.details.NewsDetailsModel
import com.tangem.features.feed.ui.LocalIsOpenedInBottomSheet
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(
@ -50,15 +50,11 @@ 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 = if (LocalIsOpenedInBottomSheet.current) {
TangemTopBarType.BottomSheet
} else {
TangemTopBarType.Default
},
type = TangemTopBarType.BottomSheet,
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
@ -66,10 +62,8 @@ internal class DefaultNewsDetailsComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = state.onBackClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
@ -84,10 +78,8 @@ internal class DefaultNewsDetailsComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = state.onShareClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED &&

View file

@ -10,8 +10,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
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
@ -31,8 +33,8 @@ import com.tangem.core.ui.res.LocalRedesignEnabled
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.LocalIsOpenedInBottomSheet
import com.tangem.features.feed.ui.news.list.NewsListContent
import dev.chrisbanes.haze.HazeProgressive
import kotlinx.serialization.Serializable
internal class DefaultNewsListComponent(
@ -48,16 +50,13 @@ internal class DefaultNewsListComponent(
val state by newsListModel.state.collectAsStateWithLifecycle()
if (LocalRedesignEnabled.current) {
TangemTopBar(
modifier = Modifier.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = .2f,
preferPerformance = true,
)
backgroundColor = background
},
modifier = Modifier.background(background.copy(alpha = .95f)),
title = resourceReference(R.string.common_news),
type = TangemTopBarType.BottomSheet,
type = if (LocalIsOpenedInBottomSheet.current) {
TangemTopBarType.BottomSheet
} else {
TangemTopBarType.Default
},
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
@ -65,10 +64,8 @@ internal class DefaultNewsListComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = state.onBackClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,

View file

@ -1,6 +1,5 @@
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
@ -13,7 +12,6 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
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.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.field.search.TangemFieldShape
@ -23,9 +21,9 @@ import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.model.search.SearchModel
import com.tangem.features.feed.ui.LocalIsOpenedInBottomSheet
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,
@ -53,15 +51,11 @@ internal class DefaultSearchComponent(
}
TangemTopBar(
modifier = Modifier.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
type = if (LocalIsOpenedInBottomSheet.current) {
TangemTopBarType.BottomSheet
} else {
TangemTopBarType.Default
},
type = TangemTopBarType.BottomSheet,
reserveSlotSpace = false,
content = {
TangemSearchField(
@ -110,7 +104,6 @@ internal class DefaultSearchComponent(
params = SearchTokenSelectorComponent.Params(
entries = config.entries,
appCurrency = config.appCurrency,
isBalanceHidden = config.isBalanceHidden,
onTokenSelected = config.onTokenSelected,
onDismiss = config.onDismiss,
),

View file

@ -8,7 +8,6 @@ internal sealed interface SearchBottomSheetRoute {
data class TokenSelector(
val entries: List<UserAssetEntry>,
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val onTokenSelected: (UserAssetEntry) -> Unit,
val onDismiss: () -> Unit,
) : SearchBottomSheetRoute

View file

@ -39,7 +39,6 @@ internal class SearchTokenSelectorComponent @AssistedInject constructor(
data class Params(
val entries: List<UserAssetEntry>,
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val onTokenSelected: (UserAssetEntry) -> Unit,
val onDismiss: () -> Unit,
)

View file

@ -87,7 +87,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
getUserCountryUseCase: GetUserCountryUseCase,
paramsContainer: ParamsContainer,
private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
addToPortfolioManagerFactory: AddToPortfolioManager.Factory,
private val designFeatureToggles: DesignFeatureToggles,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
@ -247,6 +247,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
val state = MutableStateFlow(
MarketsTokenDetailsUM(
tokenName = params.token.name,
symbol = params.token.symbol,
priceText = params.token.tokenQuotes.currentPrice.format {
fiat(
fiatCurrencyCode = currentAppCurrency.value.code,

View file

@ -87,6 +87,7 @@ internal class NewsListModel @Inject constructor(
init {
observeNewsList()
batchFlowManager.reload()
loadCategories()
}

View file

@ -229,7 +229,6 @@ internal class SearchModel @Inject constructor(
SearchBottomSheetRoute.TokenSelector(
entries = grouped.entries,
appCurrency = currentAppCurrency.value,
isBalanceHidden = isBalanceHidden.value,
onTokenSelected = ::onTokenSelectedFromGroup,
onDismiss = { bottomSheetNavigation.dismiss() },
),

View file

@ -2,22 +2,37 @@ package com.tangem.features.feed.model.search
import androidx.compose.runtime.Stable
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletIconUseCase
import com.tangem.features.feed.components.search.SearchTokenSelectorComponent
import com.tangem.features.feed.model.search.state.TokenSelectorStateController
import com.tangem.features.feed.model.search.state.transformers.BuildTokenSelectorSectionsTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class SearchTokenSelectorModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
userWalletsListRepository: UserWalletsListRepository,
private val stateController: TokenSelectorStateController,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val walletIconUMConverter: WalletIconUMConverter,
) : Model() {
private val params = paramsContainer.require<SearchTokenSelectorComponent.Params>()
@ -26,11 +41,35 @@ internal class SearchTokenSelectorModel @Inject constructor(
get() = stateController.uiState
init {
val requiredWalletIds = params.entries.map { it.userWalletId }.toSet()
val walletIconsFlow = userWalletsListRepository.userWallets
.filterNotNull()
.map { wallets ->
wallets
.filter { it.walletId in requiredWalletIds }
.associate { wallet ->
wallet.walletId to walletIconUMConverter.convert(getWalletIconUseCase(wallet))
}
}
modelScope.launch(dispatchers.default) {
combine(
walletIconsFlow,
getBalanceHidingSettingsUseCase.isBalanceHidden(),
::Pair,
).collect { (walletIcons, isBalanceHidden) ->
rebuildSections(isBalanceHidden, walletIcons)
}
}
}
private fun rebuildSections(isBalanceHidden: Boolean, walletIcons: Map<UserWalletId, DeviceIconUM>) {
stateController.update(
BuildTokenSelectorSectionsTransformer(
entries = params.entries,
appCurrency = params.appCurrency,
isBalanceHidden = params.isBalanceHidden,
isBalanceHidden = isBalanceHidden,
walletIcons = walletIcons,
onTokenSelected = params.onTokenSelected,
),
)

View file

@ -4,14 +4,17 @@ import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.markets.tokenselector.AccountHeaderData
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
import com.tangem.common.ui.markets.tokenselector.TokenSelectorSectionUM
import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.portfolio.UserAssetEntry
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.collections.immutable.toImmutableList
internal class BuildTokenSelectorSectionsTransformer(
private val entries: List<UserAssetEntry>,
private val appCurrency: AppCurrency,
private val isBalanceHidden: Boolean,
private val walletIcons: Map<UserWalletId, DeviceIconUM>,
private val onTokenSelected: (UserAssetEntry) -> Unit,
) : TokenSelectorUMTransformer {
@ -30,11 +33,12 @@ internal class BuildTokenSelectorSectionsTransformer(
val byWallet = entries.groupBy { it.userWalletId }
val shouldShowWalletHeaders = byWallet.size > 1
for ((_, walletEntries) in byWallet) {
for ((walletId, walletEntries) in byWallet) {
if (shouldShowWalletHeaders) {
sections.add(
TokenSelectorSectionUM.WalletHeader(
walletName = walletEntries.first().userWalletName,
deviceIcon = walletIcons[walletId] ?: DeviceIconUM.Stub(cardsCount = 1),
),
)
}

View file

@ -18,17 +18,29 @@ 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.components.topFade
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.extensions.conditionalCompose
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
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.features.feed.components.FeedEntryChildFactory
import com.tangem.features.feed.ui.utils.FadeConstants.BASE_FADE_LEVEL
import com.tangem.features.feed.ui.utils.contentFeedEntryStackAnimation
import com.tangem.features.feed.ui.utils.topBarFeedEntryAnimatedContentTransitionSpec
import dev.chrisbanes.haze.rememberHazeState
/**
* When the value is `null` (default), the fade covers `topBarHeight`. A screen with sticky chrome
* (e.g. category chips, sort options) can set this to `0.dp` to disable the centralized fade and
* apply its own fade on its inner haze source covering the full sticky header (topbar + chrome).
*/
internal val LocalContentTopFadeHeightOverride = compositionLocalOf<MutableState<Dp?>?> { null }
internal val LocalIsOpenedInBottomSheet = staticCompositionLocalOf { true }
@OptIn(ExperimentalDecomposeApi::class)
@Composable
internal fun EntryContent(
@ -38,22 +50,22 @@ internal fun EntryContent(
onExpandSheet: () -> Unit,
isOpenedInBottomSheet: Boolean,
) {
if (LocalRedesignEnabled.current) {
EntryContentV2(
bottomSheetState = bottomSheetState,
stackState = stackState,
onHeaderSizeChange = onHeaderSizeChange,
onExpandSheet = onExpandSheet,
isOpenedInBottomSheet = isOpenedInBottomSheet,
)
} else {
EntryContentV1(
bottomSheetState = bottomSheetState,
stackState = stackState,
onHeaderSizeChange = onHeaderSizeChange,
onExpandSheet = onExpandSheet,
isOpenedInBottomSheet = isOpenedInBottomSheet,
)
CompositionLocalProvider(LocalIsOpenedInBottomSheet provides isOpenedInBottomSheet) {
if (LocalRedesignEnabled.current) {
EntryContentV2(
bottomSheetState = bottomSheetState,
stackState = stackState,
onHeaderSizeChange = onHeaderSizeChange,
onExpandSheet = onExpandSheet,
)
} else {
EntryContentV1(
bottomSheetState = bottomSheetState,
stackState = stackState,
onHeaderSizeChange = onHeaderSizeChange,
onExpandSheet = onExpandSheet,
)
}
}
}
@ -63,11 +75,11 @@ private fun EntryContentV1(
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
onHeaderSizeChange: (Dp) -> Unit,
onExpandSheet: () -> Unit,
isOpenedInBottomSheet: Boolean,
) {
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
val stackAnimation = remember { contentFeedEntryStackAnimation() }
val isOpenedInBottomSheet = LocalIsOpenedInBottomSheet.current
Surface(contentColor = background) {
Scaffold(
@ -125,75 +137,120 @@ private fun EntryContentV2(
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
onHeaderSizeChange: (Dp) -> Unit,
onExpandSheet: () -> 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()
val fadeHeightOverride = remember { mutableStateOf<Dp?>(null) }
val statusBarInset = if (LocalIsOpenedInBottomSheet.current) {
0.dp
} else {
WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
}
val effectiveTopBarHeight = topBarHeight + statusBarInset
val effectiveFadeHeight = fadeHeightOverride.value ?: effectiveTopBarHeight
val isTopFadeSolid = LocalIsOpenedInBottomSheet.current && bottomSheetState.value == BottomSheetState.COLLAPSED
Surface(contentColor = background) {
CompositionLocalProvider(LocalHazeState provides hazeState) {
Surface(color = background, contentColor = background) {
CompositionLocalProvider(
LocalHazeState provides hazeState,
LocalContentTopFadeHeightOverride provides fadeHeightOverride,
LocalBottomSheetTopFadeSolid provides isTopFadeSolid,
) {
Box(modifier = Modifier.fillMaxSize()) {
Children(
modifier = Modifier.fillMaxSize(),
stack = stackState.value,
animation = animationContent,
) { child ->
child.instance.Content(
modifier = Modifier
.fillMaxSize()
.conditionalCompose(
condition = !isOpenedInBottomSheet,
modifier = {
padding(top = topBarHeight)
},
)
.hazeSourceTangem(zIndex = 0f, state = hazeState),
contentPadding = PaddingValues(top = topBarHeight),
bottomSheetState = bottomSheetState,
)
}
Box(
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)
}
}
},
) {
AnimatedContent(
targetState = stackState.value.active,
transitionSpec = animationAppBar,
contentKey = { it.key },
label = "FeedEntryAppBar",
) { state ->
state.instance.Title(bottomSheetState)
}
CollapsedTitleClickOverlay(
bottomSheetState = bottomSheetState,
onExpandSheet = onExpandSheet,
)
}
ContentBlock(
bottomSheetState = bottomSheetState,
effectiveFadeHeight = effectiveFadeHeight,
stackState = stackState,
topBarHeight = effectiveTopBarHeight,
)
TitleBlock(
bottomSheetState = bottomSheetState,
stackState = stackState,
onTopBarHeightChang = { dp ->
onHeaderSizeChange(dp)
topBarHeight = dp
},
onExpandSheet = onExpandSheet,
)
}
}
}
}
@Composable
private fun BoxScope.TitleBlock(
bottomSheetState: State<BottomSheetState>,
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
onTopBarHeightChang: (Dp) -> Unit,
onExpandSheet: () -> Unit,
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current
val animationAppBar = remember(stackState) { topBarFeedEntryAnimatedContentTransitionSpec(stackState) }
val isOpenedInBottomSheet = LocalIsOpenedInBottomSheet.current
Box(
modifier = modifier
.align(Alignment.TopStart)
.then(if (!isOpenedInBottomSheet) Modifier.statusBarsPadding() else Modifier),
) {
Box(
modifier = Modifier.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {
with(density) {
val height = coordinates.size.height.toDp()
onTopBarHeightChang(height)
}
}
},
) {
AnimatedContent(
targetState = stackState.value.active,
transitionSpec = animationAppBar,
contentKey = { it.key },
label = "FeedEntryAppBar",
) { state ->
state.instance.Title(bottomSheetState)
}
CollapsedTitleClickOverlay(
bottomSheetState = bottomSheetState,
onExpandSheet = onExpandSheet,
)
}
}
}
@Composable
private fun BoxScope.ContentBlock(
bottomSheetState: State<BottomSheetState>,
effectiveFadeHeight: Dp,
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
topBarHeight: Dp,
modifier: Modifier = Modifier,
) {
val animationContent = remember { contentFeedEntryStackAnimation() }
Children(
modifier = modifier.fillMaxSize(),
stack = stackState.value,
animation = animationContent,
) { child ->
child.instance.Content(
modifier = Modifier
.fillMaxSize()
.hazeSourceTangem(zIndex = 0f, state = LocalHazeState.current)
.topFade(
height = effectiveFadeHeight,
color = feedTopFadeColor(TangemTheme.colors2.surface.level2.copy(BASE_FADE_LEVEL)),
solidStop = feedTopFadeSolidStop(),
),
contentPadding = PaddingValues(top = topBarHeight),
bottomSheetState = bottomSheetState,
)
}
}
@Composable
private fun BoxScope.CollapsedTitleClickOverlay(bottomSheetState: State<BottomSheetState>, onExpandSheet: () -> Unit) {
if (bottomSheetState.value == BottomSheetState.COLLAPSED) {

View file

@ -0,0 +1,48 @@
package com.tangem.features.feed.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.features.feed.ui.utils.FadeConstants.FIRST_STEP
import com.tangem.features.feed.ui.utils.FadeConstants.FIRST_STEP_FADE_LEVEL
/**
* When `true`, top fade areas render as a solid color (no gradient) used while the wallet
* bottom sheet is collapsed so the peek header matches [LocalMainBottomSheetColor].
*/
internal val LocalBottomSheetTopFadeSolid = compositionLocalOf { false }
private const val EXPANDED_TOP_FADE_SOLID_STOP = 0.6f
private const val COLLAPSED_TOP_FADE_SOLID_STOP = 1f
@Composable
internal fun feedTopFadeSolidStop(): Float {
return if (LocalBottomSheetTopFadeSolid.current) {
COLLAPSED_TOP_FADE_SOLID_STOP
} else {
EXPANDED_TOP_FADE_SOLID_STOP
}
}
@Composable
internal fun feedTopFadeColor(defaultFadeColor: Color): Color {
return if (LocalBottomSheetTopFadeSolid.current) {
LocalMainBottomSheetColor.current.value
} else {
defaultFadeColor
}
}
@Composable
internal fun feedTopFadeColorStops(defaultFadeColor: Color): Array<Pair<Float, Color>> {
if (LocalBottomSheetTopFadeSolid.current) {
val solidColor = LocalMainBottomSheetColor.current.value
return arrayOf(0f to solidColor, 1f to solidColor)
}
return arrayOf(
0f to defaultFadeColor,
FIRST_STEP to defaultFadeColor.copy(FIRST_STEP_FADE_LEVEL),
1f to Color.Transparent,
)
}

View file

@ -16,12 +16,14 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.ui.LocalIsOpenedInBottomSheet
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
@Composable
@ -96,7 +98,7 @@ private fun FeedSearchBarV2(
modifier = modifier,
startContent = startContent,
endContent = endContent,
type = TangemTopBarType.BottomSheet,
type = if (LocalIsOpenedInBottomSheet.current) TangemTopBarType.BottomSheet else TangemTopBarType.Default,
reserveSlotSpace = false,
content = {
Row(
@ -107,7 +109,9 @@ private fun FeedSearchBarV2(
end = if (endContent != null) TangemTheme.dimens2.x3 else 0.dp,
)
.clip(CircleShape)
.background(color = TangemTheme.colors2.button.backgroundSecondary)
.hazeEffectTangem {
blurRadius = 8.dp
}
.conditional(condition = isSearchBarClickable) {
clickable(onClick = feedListSearchBar.onBarClick)
}

View file

@ -266,13 +266,13 @@ private fun LazyListScope.bestOpportunitiesItemsV2(state: EarnBestOpportunitiesU
is EarnBestOpportunitiesUM.Empty -> {
item(key = "best_opportunities_empty") {
SpacerH(12.dp)
BestOpportunitiesEmpty() // TODO in [REDACTED_TASK_KEY]
BestOpportunitiesEmpty()
}
}
is EarnBestOpportunitiesUM.EmptyFiltered -> {
item(key = "best_opportunities_empty_filtered") {
SpacerH(12.dp)
BestOpportunitiesEmptyFiltered(onClearFilterClick = state.onClearFilterClick) // TODO in [REDACTED_TASK_KEY]
BestOpportunitiesEmptyFiltered(onClearFilterClick = state.onClearFilterClick)
}
}
is EarnBestOpportunitiesUM.Content -> {
@ -305,7 +305,7 @@ private fun LazyListScope.bestOpportunitiesItemsV2(state: EarnBestOpportunitiesU
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.padding(vertical = 142.dp, horizontal = 114.dp),
.padding(vertical = 142.dp, horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(onRetryClick = state.onRetryClicked)

View file

@ -85,7 +85,7 @@ private fun BestOpportunitiesEmptyV2(modifier: Modifier = Modifier) {
Text(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x8),
text = stringResourceSafe(R.string.earn_empty),
style = TangemTheme.typography2.bodyRegular14,
style = TangemTheme.typography2.subheadlineMedium14,
color = TangemTheme.colors2.text.neutral.secondary,
textAlign = TextAlign.Center,
)

View file

@ -44,7 +44,7 @@ private fun BestOpportunitiesEmptyFilteredV2(onClearFilterClick: () -> Unit, mod
) {
Text(
text = stringResourceSafe(R.string.earn_no_results),
style = TangemTheme.typography2.bodyRegular14,
style = TangemTheme.typography2.subheadlineMedium14,
color = TangemTheme.colors2.text.neutral.secondary,
)
SpacerH(TangemTheme.dimens2.x2)

View file

@ -175,13 +175,15 @@ private fun NetworksTypesBlock(
overflow = TextOverflow.Ellipsis,
)
TangemCheckbox(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.TAIL),
isChecked = item.isSelected,
onCheckedChange = { onOptionClick(item) },
)
if (item.isSelected) {
TangemCheckbox(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.TAIL),
isChecked = true,
onCheckedChange = { onOptionClick(item) },
)
}
}
}
}
@ -233,13 +235,15 @@ private fun SpecificNetworksBlock(
overflow = TextOverflow.Ellipsis,
)
TangemCheckbox(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.TAIL),
isChecked = item.isSelected,
onCheckedChange = { onOptionClick(item) },
)
if (item.isSelected) {
TangemCheckbox(
modifier = Modifier
.padding(start = 8.dp)
.layoutId(layoutId = TangemRowLayoutId.TAIL),
isChecked = true,
onCheckedChange = { onOptionClick(item) },
)
}
}
}
}

View file

@ -115,7 +115,8 @@ private fun EarnListItemV2(item: EarnListItemUM, modifier: Modifier = Modifier)
tangemIconUM = TangemIconUM.Currency(item.currencyIconState),
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x2),
.padding(end = TangemTheme.dimens2.x2)
.size(TangemTheme.dimens2.x10),
)
TokenTitle(

View file

@ -15,10 +15,10 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.ds.opportunities.OpportunitiesBG
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.opportunities.OpportunitiesBG
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.*
@ -53,10 +53,10 @@ private fun MostlyUsedCardV2(item: EarnListItemUM, onClick: () -> Unit, modifier
) {
Column(modifier = Modifier.padding(12.dp)) {
CurrencyIcon(
modifier = Modifier.size(32.dp),
state = item.currencyIconState,
shouldDisplayNetwork = true,
networkBadgeSize = 12.dp,
networkBadgeSize = TangemTheme.dimens2.x4,
iconSize = TangemTheme.dimens2.x10,
networkBadgeBackground = TangemTheme.colors.background.action,
)

View file

@ -1,7 +1,6 @@
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
@ -16,9 +15,7 @@ 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
@ -31,7 +28,6 @@ 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(
@ -39,26 +35,10 @@ internal fun FeedListHeader(
feedListSearchBar: FeedListSearchBar,
modifier: Modifier = Modifier,
) {
val background = LocalMainBottomSheetColor.current.value
FeedSearchBar(
isSearchBarClickable = isSearchBarClickable,
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),
modifier = modifier.testTag(SEARCH_BAR),
)
}

View file

@ -118,12 +118,7 @@ private fun Content(
contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()),
) {
item("header") {
Header(
state = state,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
Header(state = state)
}
item {
if (isRedesignEnabled) SpacerH(TangemTheme.dimens2.x3) else SpacerH16()
@ -202,15 +197,33 @@ internal fun MarketsTokenDetailsTopBar(
}
@Composable
private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) {
private fun Header(state: MarketsTokenDetailsUM) {
if (LocalRedesignEnabled.current) {
HeaderV2(
modifier = Modifier
.padding(TangemTheme.dimens2.x4)
.fillMaxWidth(),
state = state,
)
} else {
HeaderV1(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
state = state,
)
}
}
@Composable
private fun HeaderV1(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = Modifier.weight(1f)) {
TokenPriceText(
TokenPriceTextV1(
price = state.priceText,
priceAnnotated = state.priceAnnotated,
triggerPriceChange = state.triggerPriceChange,
)
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
@ -240,23 +253,55 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier)
}
@Composable
private fun TokenPriceText(
price: String,
triggerPriceChange: StateEvent<PriceChangeType>,
priceAnnotated: TextReference,
modifier: Modifier = Modifier,
) {
if (LocalRedesignEnabled.current) {
TokenPriceTextV2(
priceAnnotated = priceAnnotated,
triggerPriceChange = triggerPriceChange,
modifier = modifier,
)
} else {
TokenPriceTextV1(
price = price,
triggerPriceChange = triggerPriceChange,
modifier = modifier,
private fun HeaderV2(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = Modifier.weight(1f)) {
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
Text(
text = state.tokenName,
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
)
Text(
text = state.symbol,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.tertiary,
)
}
SpacerH(TangemTheme.dimens2.x1)
TokenPriceTextV2(
priceAnnotated = state.priceAnnotated,
triggerPriceChange = state.triggerPriceChange,
)
SpacerH(TangemTheme.dimens2.x4)
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1)) {
Text(
text = state.dateTimeText.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.primary,
)
if (state.priceChangePercentText != null) {
PriceChangeInPercent(
valueInPercent = state.priceChangePercentText,
type = state.priceChangeType,
textStyle = TangemTheme.typography2.captionMedium12,
)
}
}
}
SpacerW4()
CoinIcon(
modifier = Modifier.requiredSize(70.dp),
url = state.iconUrl,
alpha = 1f,
colorFilter = null,
fallbackResId = R.drawable.ic_custom_token_44,
)
}
}
@ -323,9 +368,9 @@ private fun TokenPriceTextV2(
text = priceAnnotated.resolveAnnotatedReference(),
modifier = modifier,
color = color.value,
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography2.headingBold34.fontSize),
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography2.titleRegular44.fontSize),
maxLines = 1,
style = TangemTheme.typography2.headingBold34,
style = TangemTheme.typography2.titleRegular44,
)
}

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
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
@ -49,6 +50,7 @@ import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.market.detailed.state.ExchangeItemUM
import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent
@ -273,7 +275,7 @@ private fun ErrorV2(content: ExchangesBottomSheetContent.Error, modifier: Modifi
text = stringResourceSafe(id = content.message),
color = TangemTheme.colors2.text.neutral.tertiary,
textAlign = TextAlign.Center,
style = TangemTheme.typography2.bodyRegular14,
style = TangemTheme.typography2.subheadlineMedium14,
)
SpacerH(8.dp)
@ -369,6 +371,7 @@ private fun ExchangeItemRowPlaceholder(modifier: Modifier = Modifier) {
.layoutId(TangemRowLayoutId.HEAD),
)
RectangleShimmer(
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.width(108.dp)
.height(20.dp)
@ -376,6 +379,7 @@ private fun ExchangeItemRowPlaceholder(modifier: Modifier = Modifier) {
.layoutId(TangemRowLayoutId.START_TOP),
)
RectangleShimmer(
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.width(52.dp)
.height(16.dp)
@ -383,6 +387,7 @@ private fun ExchangeItemRowPlaceholder(modifier: Modifier = Modifier) {
.layoutId(TangemRowLayoutId.START_BOTTOM),
)
RectangleShimmer(
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.width(106.dp)
.height(20.dp)
@ -390,6 +395,7 @@ private fun ExchangeItemRowPlaceholder(modifier: Modifier = Modifier) {
.layoutId(TangemRowLayoutId.END_TOP),
)
RectangleShimmer(
radius = TangemTheme.dimens2.x25,
modifier = Modifier
.width(52.dp)
.height(16.dp)
@ -428,6 +434,25 @@ private fun Preview_ExchangesBottomSheet(
}
}
@Preview
@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ExchangesBottomSheetV2(
@PreviewParameter(ExchangesBottomSheetContentProvider::class) content: ExchangesBottomSheetContent,
) {
TangemThemePreviewRedesign {
CompositionLocalProvider(LocalRedesignEnabled provides true) {
ExchangesBottomSheet(
config = TangemBottomSheetConfig(
onDismissRequest = {},
content = content,
isShown = true,
),
)
}
}
}
private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterProvider<ExchangesBottomSheetContent>(
listOf(
ExchangesBottomSheetContent.Loading(exchangesCount = 13),

View file

@ -20,6 +20,7 @@ internal object MarketsTokenDetailsPreview {
val loadingState = MarketsTokenDetailsUM(
tokenName = "Token Name",
symbol = "USDT",
priceText = "$0.00000000324",
dateTimeText = stringReference("Today"),
priceChangePercentText = "52.00%",
@ -55,6 +56,7 @@ internal object MarketsTokenDetailsPreview {
val contentState = MarketsTokenDetailsUM(
tokenName = "Token Name",
symbol = "USDT",
priceText = "$0.00000000324",
dateTimeText = stringReference("Today"),
priceChangePercentText = "52.00%",

View file

@ -14,6 +14,7 @@ import java.math.BigDecimal
internal data class MarketsTokenDetailsUM(
val tokenName: String,
val symbol: String,
val priceText: String,
val priceAnnotated: TextReference,
val iconUrl: String?,

View file

@ -28,11 +28,9 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
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.event.consumedEvent
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
@ -41,10 +39,11 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.market.list.state.*
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import com.tangem.features.feed.ui.feedTopFadeColorStops
import com.tangem.features.feed.ui.market.list.components.MarketsListLazyColumn
import com.tangem.features.feed.ui.market.list.components.MarketsListSortByBottomSheet
import com.tangem.features.feed.ui.market.list.components.Options
import dev.chrisbanes.haze.rememberHazeState
import com.tangem.features.feed.ui.utils.FadeConstants.BASE_FADE_LEVEL
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
@ -108,19 +107,16 @@ internal fun TopBarWithSearch(
@Composable
internal fun MarketsList(contentPadding: PaddingValues, state: MarketsListUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
// should use here new overrided haze state cause on level upper already applied
CompositionLocalProvider(LocalHazeState provides rememberHazeState()) {
Column(
modifier = modifier
.fillMaxSize()
.imePadding()
.drawBehind { drawRect(background) },
) {
Content(state = state, contentPadding = contentPadding)
}
MarketsListSortByBottomSheet(config = state.sortByBottomSheet)
KeyboardEvents(isSortByBottomSheetShown = state.sortByBottomSheet.isShown)
Column(
modifier = modifier
.fillMaxSize()
.imePadding()
.drawBehind { drawRect(background) },
) {
Content(state = state, contentPadding = contentPadding)
}
MarketsListSortByBottomSheet(config = state.sortByBottomSheet)
KeyboardEvents(isSortByBottomSheetShown = state.sortByBottomSheet.isShown)
}
@Suppress("LongMethod")
@ -205,21 +201,26 @@ private fun ColumnScope.ContentV2(contentPadding: PaddingValues, state: MarketsL
val scrolledState = remember { mutableStateOf(false) }
var optionsHeight by remember { mutableStateOf(0.dp) }
val density = LocalDensity.current
val fadeColor = TangemTheme.colors2.surface.level2.copy(BASE_FADE_LEVEL)
val topPadding = contentPadding.calculateTopPadding()
Box(modifier = Modifier.fillMaxSize()) {
ItemsList(
topContentPadding = contentPadding.calculateTopPadding() + optionsHeight,
modifier = Modifier
.align(Alignment.TopStart)
.hazeSourceTangem(zIndex = 1f),
modifier = Modifier.align(Alignment.TopStart),
topContentPadding = topPadding + optionsHeight,
scrolledState = scrolledState,
isInSearchMode = state.isInSearchMode,
state = state.list,
)
TopFade(
modifier = Modifier.padding(top = topPadding),
colorStops = feedTopFadeColorStops(fadeColor),
height = TangemTheme.dimens2.x4 + optionsHeight,
)
Options(
modifier = Modifier
.align(Alignment.TopStart)
.padding(bottom = TangemTheme.dimens2.x4, top = contentPadding.calculateTopPadding())
.padding(bottom = TangemTheme.dimens2.x4, top = topPadding)
.padding(horizontal = TangemTheme.dimens2.x4)
.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {

View file

@ -1,6 +1,5 @@
package com.tangem.features.feed.ui.market.list.components
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@ -20,14 +19,12 @@ import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.tabs.TangemSegmentUM
import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.market.list.state.MarketsListUM
import com.tangem.features.feed.model.market.list.state.SortByMenuUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import dev.chrisbanes.haze.HazeProgressive
import kotlinx.collections.immutable.persistentListOf
import com.tangem.core.ui.ds.button.TangemButtonIconPosition as RedesignTangemButtonIconPosition
@ -120,7 +117,6 @@ private fun OptionsV2(
modifier: Modifier = Modifier,
) {
var isShowDropdownMenu by rememberSaveable { mutableStateOf(false) }
val background = LocalMainBottomSheetColor.current.value
val segmentItems = remember {
persistentListOf(
@ -147,16 +143,7 @@ private fun OptionsV2(
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Max)
.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .2f,
endIntensity = 0f,
easing = EaseOut,
preferPerformance = true,
)
backgroundColor = background
},
.height(IntrinsicSize.Max),
horizontalArrangement = Arrangement.SpaceBetween,
) {
PrimaryInverseTangemButton(

View file

@ -1,11 +1,25 @@
package com.tangem.features.feed.ui.market.list.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.contextmenu.TangemContextMenu
import com.tangem.core.ui.ds.contextmenu.TangemContextMenuCheckboxItem
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.model.market.list.state.SortByMenuUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
@ -22,15 +36,61 @@ internal fun SortByMenu(
offset = DpOffset.Zero,
modifier = modifier,
) {
SortByTypeUM.entries.fastForEach { sortType ->
TangemContextMenuCheckboxItem(
title = sortType.text,
isChecked = sortMenuUM.selectedOption == sortType,
onClick = {
sortMenuUM.onOptionClicked(sortType)
onDropdownDismiss()
},
)
SortByTypeUM.entries.fastForEachIndexed { index, sortType ->
Column {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.widthIn(238.dp)
.clickableSingle(
onClick = {
sortMenuUM.onOptionClicked(sortType)
onDropdownDismiss()
},
)
.padding(
vertical = TangemTheme.dimens2.x5,
horizontal = TangemTheme.dimens2.x4,
),
) {
Text(
text = sortType.text.resolveReference(),
style = TangemTheme.typography2.headingSemibold17,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
if (sortMenuUM.selectedOption == sortType) {
Box(
modifier = Modifier
.padding(TangemTheme.dimens2.x0_5)
.size(TangemTheme.dimens2.x5)
.background(
color = TangemTheme.colors2.graphic.neutral.primary,
shape = CircleShape,
),
) {
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_check_default_24),
),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primaryInverted,
modifier = Modifier
.align(Alignment.Center)
.padding(TangemTheme.dimens2.x0_5)
.size(TangemTheme.dimens2.x4),
)
}
}
}
if (index < SortByTypeUM.entries.size - 1) {
HorizontalDivider(
thickness = 0.5.dp,
color = TangemTheme.colors2.border.neutral.quaternary,
)
}
}
}
}
}

View file

@ -1,6 +1,5 @@
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
@ -15,20 +14,23 @@ import androidx.compose.ui.platform.LocalDensity
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.TopFade
import com.tangem.core.ui.components.chip.Chip
import com.tangem.core.ui.components.chip.entity.ChipUM
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.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.features.feed.ui.feedTopFadeColorStops
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
import dev.chrisbanes.haze.HazeProgressive
import com.tangem.features.feed.ui.utils.FadeConstants.BASE_FADE_LEVEL
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableSet
@ -38,6 +40,7 @@ internal fun NewsListContent(contentPadding: PaddingValues, state: NewsListUM, m
NewsListContentV2(
contentPadding = contentPadding,
state = state,
modifier = modifier,
)
} else {
NewsListContentV1(
@ -87,50 +90,48 @@ internal fun NewsListContentV1(contentPadding: PaddingValues, state: NewsListUM,
}
@Composable
internal fun NewsListContentV2(contentPadding: PaddingValues, state: NewsListUM) {
internal fun NewsListContentV2(contentPadding: PaddingValues, state: NewsListUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
val lazyListState = rememberLazyListState()
val chipsListState = rememberLazyListState()
var chipsHeight by remember { mutableStateOf(0.dp) }
val density = LocalDensity.current
val topPadding = contentPadding.calculateTopPadding()
val fadeColor = TangemTheme.colors2.surface.level2.copy(BASE_FADE_LEVEL)
ScrollChipsToSelected(state = state, chipsListState = chipsListState)
Box(
modifier = Modifier
modifier = modifier
.fillMaxSize()
.background(background),
) {
NewsListLazyColumn(
topContentPadding = contentPadding.calculateTopPadding() + 16.dp + chipsHeight,
modifier = Modifier
.hazeSourceTangem(zIndex = 0f)
.align(Alignment.TopStart),
topContentPadding = topPadding + TangemTheme.dimens2.x4 + chipsHeight,
modifier = Modifier.align(Alignment.TopStart),
newsListState = state.newsListState,
listOfArticles = state.listOfArticles,
lazyListState = lazyListState,
onArticleClick = state.onArticleClick,
)
TopFade(
modifier = Modifier.padding(top = topPadding),
colorStops = feedTopFadeColorStops(fadeColor),
height = TangemTheme.dimens2.x4 + chipsHeight,
)
LazyRow(
state = chipsListState,
modifier = Modifier
.align(Alignment.TopStart)
.padding(top = contentPadding.calculateTopPadding(), bottom = TangemTheme.dimens2.x4)
.padding(top = topPadding, bottom = TangemTheme.dimens2.x4)
.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {
with(density) {
chipsHeight = coordinates.size.height.toDp()
}
}
}
.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .2f,
endIntensity = 0f,
easing = EaseOut,
preferPerformance = true,
)
backgroundColor = background
},
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),

View file

@ -0,0 +1,7 @@
package com.tangem.features.feed.ui.utils
internal object FadeConstants {
const val BASE_FADE_LEVEL = .95f
const val FIRST_STEP = .8f
const val FIRST_STEP_FADE_LEVEL = .7f
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.portfolio.UserAssetEntry
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM
import com.tangem.common.ui.markets.tokenselector.TokenSelectorSectionUM
import com.tangem.core.ui.ds.image.DeviceIconUM
import io.mockk.*
import kotlinx.collections.immutable.persistentListOf
import org.junit.jupiter.api.BeforeEach
@ -215,7 +216,10 @@ class BuildTokenSelectorSectionsTransformerTest {
val prevStateWithSections = TokenSelectorContentUM(
sections = persistentListOf(
TokenSelectorSectionUM.WalletHeader(walletName = "Old Wallet"),
TokenSelectorSectionUM.WalletHeader(
walletName = "Old Wallet",
deviceIcon = DeviceIconUM.Stub(cardsCount = 1),
),
),
)
@ -236,6 +240,7 @@ class BuildTokenSelectorSectionsTransformerTest {
entries = entries,
appCurrency = appCurrency,
isBalanceHidden = false,
walletIcons = emptyMap(),
onTokenSelected = onTokenSelected,
)
}